3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2016 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2016-02-17T02:03:23Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
64 * Generate a unique ID for element
66 * @return {String} [id]
68 OO
.ui
.generateElementId = function () {
70 return 'oojsui-' + OO
.ui
.elementId
;
74 * Check if an element is focusable.
75 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
77 * @param {jQuery} element Element to test
80 OO
.ui
.isFocusableElement = function ( $element
) {
82 element
= $element
[ 0 ];
84 // Anything disabled is not focusable
85 if ( element
.disabled
) {
89 // Check if the element is visible
91 // This is quicker than calling $element.is( ':visible' )
92 $.expr
.filters
.visible( element
) &&
93 // Check that all parents are visible
94 !$element
.parents().addBack().filter( function () {
95 return $.css( this, 'visibility' ) === 'hidden';
101 // Check if the element is ContentEditable, which is the string 'true'
102 if ( element
.contentEditable
=== 'true' ) {
106 // Anything with a non-negative numeric tabIndex is focusable.
107 // Use .prop to avoid browser bugs
108 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
112 // Some element types are naturally focusable
113 // (indexOf is much faster than regex in Chrome and about the
114 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
115 nodeName
= element
.nodeName
.toLowerCase();
116 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
120 // Links and areas are focusable if they have an href
121 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
129 * Find a focusable child
131 * @param {jQuery} $container Container to search in
132 * @param {boolean} [backwards] Search backwards
133 * @return {jQuery} Focusable child, an empty jQuery object if none found
135 OO
.ui
.findFocusable = function ( $container
, backwards
) {
136 var $focusable
= $( [] ),
137 // $focusableCandidates is a superset of things that
138 // could get matched by isFocusableElement
139 $focusableCandidates
= $container
140 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
143 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
146 $focusableCandidates
.each( function () {
147 var $this = $( this );
148 if ( OO
.ui
.isFocusableElement( $this ) ) {
157 * Get the user's language and any fallback languages.
159 * These language codes are used to localize user interface elements in the user's language.
161 * In environments that provide a localization system, this function should be overridden to
162 * return the user's language(s). The default implementation returns English (en) only.
164 * @return {string[]} Language codes, in descending order of priority
166 OO
.ui
.getUserLanguages = function () {
171 * Get a value in an object keyed by language code.
173 * @param {Object.<string,Mixed>} obj Object keyed by language code
174 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
175 * @param {string} [fallback] Fallback code, used if no matching language can be found
176 * @return {Mixed} Local value
178 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
181 // Requested language
185 // Known user language
186 langs
= OO
.ui
.getUserLanguages();
187 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
194 if ( obj
[ fallback
] ) {
195 return obj
[ fallback
];
197 // First existing language
198 for ( lang
in obj
) {
206 * Check if a node is contained within another node
208 * Similar to jQuery#contains except a list of containers can be supplied
209 * and a boolean argument allows you to include the container in the match list
211 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
212 * @param {HTMLElement} contained Node to find
213 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
214 * @return {boolean} The node is in the list of target nodes
216 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
218 if ( !Array
.isArray( containers
) ) {
219 containers
= [ containers
];
221 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
222 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
230 * Return a function, that, as long as it continues to be invoked, will not
231 * be triggered. The function will be called after it stops being called for
232 * N milliseconds. If `immediate` is passed, trigger the function on the
233 * leading edge, instead of the trailing.
235 * Ported from: http://underscorejs.org/underscore.js
237 * @param {Function} func
238 * @param {number} wait
239 * @param {boolean} immediate
242 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
247 later = function () {
250 func
.apply( context
, args
);
253 if ( immediate
&& !timeout
) {
254 func
.apply( context
, args
);
256 clearTimeout( timeout
);
257 timeout
= setTimeout( later
, wait
);
262 * Proxy for `node.addEventListener( eventName, handler, true )`.
264 * @param {HTMLElement} node
265 * @param {string} eventName
266 * @param {Function} handler
269 OO
.ui
.addCaptureEventListener = function ( node
, eventName
, handler
) {
270 node
.addEventListener( eventName
, handler
, true );
274 * Proxy for `node.removeEventListener( eventName, handler, true )`.
276 * @param {HTMLElement} node
277 * @param {string} eventName
278 * @param {Function} handler
281 OO
.ui
.removeCaptureEventListener = function ( node
, eventName
, handler
) {
282 node
.removeEventListener( eventName
, handler
, true );
286 * Reconstitute a JavaScript object corresponding to a widget created by
287 * the PHP implementation.
289 * This is an alias for `OO.ui.Element.static.infuse()`.
291 * @param {string|HTMLElement|jQuery} idOrNode
292 * A DOM id (if a string) or node for the widget to infuse.
293 * @return {OO.ui.Element}
294 * The `OO.ui.Element` corresponding to this (infusable) document node.
296 OO
.ui
.infuse = function ( idOrNode
) {
297 return OO
.ui
.Element
.static.infuse( idOrNode
);
302 * Message store for the default implementation of OO.ui.msg
304 * Environments that provide a localization system should not use this, but should override
305 * OO.ui.msg altogether.
310 // Tool tip for a button that moves items in a list down one place
311 'ooui-outline-control-move-down': 'Move item down',
312 // Tool tip for a button that moves items in a list up one place
313 'ooui-outline-control-move-up': 'Move item up',
314 // Tool tip for a button that removes items from a list
315 'ooui-outline-control-remove': 'Remove item',
316 // Label for the toolbar group that contains a list of all other available tools
317 'ooui-toolbar-more': 'More',
318 // Label for the fake tool that expands the full list of tools in a toolbar group
319 'ooui-toolgroup-expand': 'More',
320 // Label for the fake tool that collapses the full list of tools in a toolbar group
321 'ooui-toolgroup-collapse': 'Fewer',
322 // Default label for the accept button of a confirmation dialog
323 'ooui-dialog-message-accept': 'OK',
324 // Default label for the reject button of a confirmation dialog
325 'ooui-dialog-message-reject': 'Cancel',
326 // Title for process dialog error description
327 'ooui-dialog-process-error': 'Something went wrong',
328 // Label for process dialog dismiss error button, visible when describing errors
329 'ooui-dialog-process-dismiss': 'Dismiss',
330 // Label for process dialog retry action button, visible when describing only recoverable errors
331 'ooui-dialog-process-retry': 'Try again',
332 // Label for process dialog retry action button, visible when describing only warnings
333 'ooui-dialog-process-continue': 'Continue',
334 // Label for the file selection widget's select file button
335 'ooui-selectfile-button-select': 'Select a file',
336 // Label for the file selection widget if file selection is not supported
337 'ooui-selectfile-not-supported': 'File selection is not supported',
338 // Label for the file selection widget when no file is currently selected
339 'ooui-selectfile-placeholder': 'No file is selected',
340 // Label for the file selection widget's drop target
341 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
345 * Get a localized message.
347 * In environments that provide a localization system, this function should be overridden to
348 * return the message translated in the user's language. The default implementation always returns
351 * After the message key, message parameters may optionally be passed. In the default implementation,
352 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
353 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
354 * they support unnamed, ordered message parameters.
356 * @param {string} key Message key
357 * @param {Mixed...} [params] Message parameters
358 * @return {string} Translated message with parameters substituted
360 OO
.ui
.msg = function ( key
) {
361 var message
= messages
[ key
],
362 params
= Array
.prototype.slice
.call( arguments
, 1 );
363 if ( typeof message
=== 'string' ) {
364 // Perform $1 substitution
365 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
366 var i
= parseInt( n
, 10 );
367 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
370 // Return placeholder if message not found
371 message
= '[' + key
+ ']';
378 * Package a message and arguments for deferred resolution.
380 * Use this when you are statically specifying a message and the message may not yet be present.
382 * @param {string} key Message key
383 * @param {Mixed...} [params] Message parameters
384 * @return {Function} Function that returns the resolved message when executed
386 OO
.ui
.deferMsg = function () {
387 var args
= arguments
;
389 return OO
.ui
.msg
.apply( OO
.ui
, args
);
396 * If the message is a function it will be executed, otherwise it will pass through directly.
398 * @param {Function|string} msg Deferred message, or message text
399 * @return {string} Resolved message
401 OO
.ui
.resolveMsg = function ( msg
) {
402 if ( $.isFunction( msg
) ) {
409 * @param {string} url
412 OO
.ui
.isSafeUrl = function ( url
) {
413 // Keep this function in sync with php/Tag.php
414 var i
, protocolWhitelist
;
416 function stringStartsWith( haystack
, needle
) {
417 return haystack
.substr( 0, needle
.length
) === needle
;
420 protocolWhitelist
= [
421 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
422 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
423 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
430 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
431 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
436 // This matches '//' too
437 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
440 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
452 * Namespace for OOjs UI mixins.
454 * Mixins are named according to the type of object they are intended to
455 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
456 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
457 * is intended to be mixed in to an instance of OO.ui.Widget.
465 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
466 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
467 * connected to them and can't be interacted with.
473 * @param {Object} [config] Configuration options
474 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
475 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
477 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
478 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
479 * @cfg {string} [text] Text to insert
480 * @cfg {Array} [content] An array of content elements to append (after #text).
481 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
482 * Instances of OO.ui.Element will have their $element appended.
483 * @cfg {jQuery} [$content] Content elements to append (after #text).
484 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
485 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
486 * Data can also be specified with the #setData method.
488 OO
.ui
.Element
= function OoUiElement( config
) {
489 // Configuration initialization
490 config
= config
|| {};
495 this.data
= config
.data
;
496 this.$element
= config
.$element
||
497 $( document
.createElement( this.getTagName() ) );
498 this.elementGroup
= null;
499 this.debouncedUpdateThemeClassesHandler
= OO
.ui
.debounce( this.debouncedUpdateThemeClasses
);
502 if ( Array
.isArray( config
.classes
) ) {
503 this.$element
.addClass( config
.classes
.join( ' ' ) );
506 this.$element
.attr( 'id', config
.id
);
509 this.$element
.text( config
.text
);
511 if ( config
.content
) {
512 // The `content` property treats plain strings as text; use an
513 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
514 // appropriate $element appended.
515 this.$element
.append( config
.content
.map( function ( v
) {
516 if ( typeof v
=== 'string' ) {
517 // Escape string so it is properly represented in HTML.
518 return document
.createTextNode( v
);
519 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
522 } else if ( v
instanceof OO
.ui
.Element
) {
528 if ( config
.$content
) {
529 // The `$content` property treats plain strings as HTML.
530 this.$element
.append( config
.$content
);
536 OO
.initClass( OO
.ui
.Element
);
538 /* Static Properties */
541 * The name of the HTML tag used by the element.
543 * The static value may be ignored if the #getTagName method is overridden.
549 OO
.ui
.Element
.static.tagName
= 'div';
554 * Reconstitute a JavaScript object corresponding to a widget created
555 * by the PHP implementation.
557 * @param {string|HTMLElement|jQuery} idOrNode
558 * A DOM id (if a string) or node for the widget to infuse.
559 * @return {OO.ui.Element}
560 * The `OO.ui.Element` corresponding to this (infusable) document node.
561 * For `Tag` objects emitted on the HTML side (used occasionally for content)
562 * the value returned is a newly-created Element wrapping around the existing
565 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
566 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
567 // Verify that the type matches up.
568 // FIXME: uncomment after T89721 is fixed (see T90929)
570 if ( !( obj instanceof this['class'] ) ) {
571 throw new Error( 'Infusion type mismatch!' );
578 * Implementation helper for `infuse`; skips the type check and has an
579 * extra property so that only the top-level invocation touches the DOM.
581 * @param {string|HTMLElement|jQuery} idOrNode
582 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
583 * when the top-level widget of this infusion is inserted into DOM,
584 * replacing the original node; or false for top-level invocation.
585 * @return {OO.ui.Element}
587 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
588 // look for a cached result of a previous infusion.
589 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
590 if ( typeof idOrNode
=== 'string' ) {
592 $elem
= $( document
.getElementById( id
) );
594 $elem
= $( idOrNode
);
595 id
= $elem
.attr( 'id' );
597 if ( !$elem
.length
) {
598 throw new Error( 'Widget not found: ' + id
);
600 if ( $elem
[ 0 ].oouiInfused
) {
601 $elem
= $elem
[ 0 ].oouiInfused
;
603 data
= $elem
.data( 'ooui-infused' );
606 if ( data
=== true ) {
607 throw new Error( 'Circular dependency! ' + id
);
610 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
611 state
= data
.gatherPreInfuseState( $elem
);
612 // restore dynamic state after the new element is re-inserted into DOM under infused parent
613 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
614 infusedChildren
= $elem
.data( 'ooui-infused-children' );
615 if ( infusedChildren
&& infusedChildren
.length
) {
616 infusedChildren
.forEach( function ( data
) {
617 var state
= data
.gatherPreInfuseState( $elem
);
618 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
624 data
= $elem
.attr( 'data-ooui' );
626 throw new Error( 'No infusion data found: ' + id
);
629 data
= $.parseJSON( data
);
633 if ( !( data
&& data
._
) ) {
634 throw new Error( 'No valid infusion data found: ' + id
);
636 if ( data
._
=== 'Tag' ) {
637 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
638 return new OO
.ui
.Element( { $element
: $elem
} );
640 parts
= data
._
.split( '.' );
641 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
642 if ( cls
=== undefined ) {
643 // The PHP output might be old and not including the "OO.ui" prefix
644 // TODO: Remove this back-compat after next major release
645 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
646 if ( cls
=== undefined ) {
647 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
651 // Verify that we're creating an OO.ui.Element instance
654 while ( parent
!== undefined ) {
655 if ( parent
=== OO
.ui
.Element
) {
660 parent
= parent
.parent
;
663 if ( parent
!== OO
.ui
.Element
) {
664 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
667 if ( domPromise
=== false ) {
669 domPromise
= top
.promise();
671 $elem
.data( 'ooui-infused', true ); // prevent loops
672 data
.id
= id
; // implicit
673 infusedChildren
= [];
674 data
= OO
.copy( data
, null, function deserialize( value
) {
676 if ( OO
.isPlainObject( value
) ) {
678 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
679 infusedChildren
.push( infused
);
680 // Flatten the structure
681 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
682 infused
.$element
.removeData( 'ooui-infused-children' );
686 return new OO
.ui
.HtmlSnippet( value
.html
);
690 // allow widgets to reuse parts of the DOM
691 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
692 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
693 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
695 // jscs:disable requireCapitalizedConstructors
696 obj
= new cls( data
);
697 // jscs:enable requireCapitalizedConstructors
698 // now replace old DOM with this new DOM.
700 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
701 // so only mutate the DOM if we need to.
702 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
703 $elem
.replaceWith( obj
.$element
);
704 // This element is now gone from the DOM, but if anyone is holding a reference to it,
705 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
706 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
707 $elem
[ 0 ].oouiInfused
= obj
.$element
;
711 obj
.$element
.data( 'ooui-infused', obj
);
712 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
713 // set the 'data-ooui' attribute so we can identify infused widgets
714 obj
.$element
.attr( 'data-ooui', '' );
715 // restore dynamic state after the new element is inserted into DOM
716 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
721 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
723 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
724 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
725 * constructor, which will be given the enhanced config.
728 * @param {HTMLElement} node
729 * @param {Object} config
732 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
737 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
738 * (and its children) that represent an Element of the same class and the given configuration,
739 * generated by the PHP implementation.
741 * This method is called just before `node` is detached from the DOM. The return value of this
742 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
743 * is inserted into DOM to replace `node`.
746 * @param {HTMLElement} node
747 * @param {Object} config
750 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
755 * Get a jQuery function within a specific document.
758 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
759 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
761 * @return {Function} Bound jQuery function
763 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
764 function wrapper( selector
) {
765 return $( selector
, wrapper
.context
);
768 wrapper
.context
= this.getDocument( context
);
771 wrapper
.$iframe
= $iframe
;
778 * Get the document of an element.
781 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
782 * @return {HTMLDocument|null} Document object
784 OO
.ui
.Element
.static.getDocument = function ( obj
) {
785 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
786 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
787 // Empty jQuery selections might have a context
794 ( obj
.nodeType
=== 9 && obj
) ||
799 * Get the window of an element or document.
802 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
803 * @return {Window} Window object
805 OO
.ui
.Element
.static.getWindow = function ( obj
) {
806 var doc
= this.getDocument( obj
);
807 return doc
.defaultView
;
811 * Get the direction of an element or document.
814 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
815 * @return {string} Text direction, either 'ltr' or 'rtl'
817 OO
.ui
.Element
.static.getDir = function ( obj
) {
820 if ( obj
instanceof jQuery
) {
823 isDoc
= obj
.nodeType
=== 9;
824 isWin
= obj
.document
!== undefined;
825 if ( isDoc
|| isWin
) {
831 return $( obj
).css( 'direction' );
835 * Get the offset between two frames.
837 * TODO: Make this function not use recursion.
840 * @param {Window} from Window of the child frame
841 * @param {Window} [to=window] Window of the parent frame
842 * @param {Object} [offset] Offset to start with, used internally
843 * @return {Object} Offset object, containing left and top properties
845 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
846 var i
, len
, frames
, frame
, rect
;
852 offset
= { top
: 0, left
: 0 };
854 if ( from.parent
=== from ) {
858 // Get iframe element
859 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
860 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
861 if ( frames
[ i
].contentWindow
=== from ) {
867 // Recursively accumulate offset values
869 rect
= frame
.getBoundingClientRect();
870 offset
.left
+= rect
.left
;
871 offset
.top
+= rect
.top
;
873 this.getFrameOffset( from.parent
, offset
);
880 * Get the offset between two elements.
882 * The two elements may be in a different frame, but in that case the frame $element is in must
883 * be contained in the frame $anchor is in.
886 * @param {jQuery} $element Element whose position to get
887 * @param {jQuery} $anchor Element to get $element's position relative to
888 * @return {Object} Translated position coordinates, containing top and left properties
890 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
891 var iframe
, iframePos
,
892 pos
= $element
.offset(),
893 anchorPos
= $anchor
.offset(),
894 elementDocument
= this.getDocument( $element
),
895 anchorDocument
= this.getDocument( $anchor
);
897 // If $element isn't in the same document as $anchor, traverse up
898 while ( elementDocument
!== anchorDocument
) {
899 iframe
= elementDocument
.defaultView
.frameElement
;
901 throw new Error( '$element frame is not contained in $anchor frame' );
903 iframePos
= $( iframe
).offset();
904 pos
.left
+= iframePos
.left
;
905 pos
.top
+= iframePos
.top
;
906 elementDocument
= iframe
.ownerDocument
;
908 pos
.left
-= anchorPos
.left
;
909 pos
.top
-= anchorPos
.top
;
914 * Get element border sizes.
917 * @param {HTMLElement} el Element to measure
918 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
920 OO
.ui
.Element
.static.getBorders = function ( el
) {
921 var doc
= el
.ownerDocument
,
922 win
= doc
.defaultView
,
923 style
= win
.getComputedStyle( el
, null ),
925 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
926 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
927 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
928 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
939 * Get dimensions of an element or window.
942 * @param {HTMLElement|Window} el Element to measure
943 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
945 OO
.ui
.Element
.static.getDimensions = function ( el
) {
947 doc
= el
.ownerDocument
|| el
.document
,
948 win
= doc
.defaultView
;
950 if ( win
=== el
|| el
=== doc
.documentElement
) {
953 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
955 top
: $win
.scrollTop(),
956 left
: $win
.scrollLeft()
958 scrollbar
: { right
: 0, bottom
: 0 },
962 bottom
: $win
.innerHeight(),
963 right
: $win
.innerWidth()
969 borders
: this.getBorders( el
),
971 top
: $el
.scrollTop(),
972 left
: $el
.scrollLeft()
975 right
: $el
.innerWidth() - el
.clientWidth
,
976 bottom
: $el
.innerHeight() - el
.clientHeight
978 rect
: el
.getBoundingClientRect()
984 * Get scrollable object parent
986 * documentElement can't be used to get or set the scrollTop
987 * property on Blink. Changing and testing its value lets us
988 * use 'body' or 'documentElement' based on what is working.
990 * https://code.google.com/p/chromium/issues/detail?id=303131
993 * @param {HTMLElement} el Element to find scrollable parent for
994 * @return {HTMLElement} Scrollable parent
996 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
999 if ( OO
.ui
.scrollableElement
=== undefined ) {
1000 body
= el
.ownerDocument
.body
;
1001 scrollTop
= body
.scrollTop
;
1004 if ( body
.scrollTop
=== 1 ) {
1005 body
.scrollTop
= scrollTop
;
1006 OO
.ui
.scrollableElement
= 'body';
1008 OO
.ui
.scrollableElement
= 'documentElement';
1012 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1016 * Get closest scrollable container.
1018 * Traverses up until either a scrollable element or the root is reached, in which case the window
1022 * @param {HTMLElement} el Element to find scrollable container for
1023 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1024 * @return {HTMLElement} Closest scrollable container
1026 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1028 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1029 props
= [ 'overflow-x', 'overflow-y' ],
1030 $parent
= $( el
).parent();
1032 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1033 props
= [ 'overflow-' + dimension
];
1036 while ( $parent
.length
) {
1037 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1038 return $parent
[ 0 ];
1042 val
= $parent
.css( props
[ i
] );
1043 if ( val
=== 'auto' || val
=== 'scroll' ) {
1044 return $parent
[ 0 ];
1047 $parent
= $parent
.parent();
1049 return this.getDocument( el
).body
;
1053 * Scroll element into view.
1056 * @param {HTMLElement} el Element to scroll into view
1057 * @param {Object} [config] Configuration options
1058 * @param {string} [config.duration='fast'] jQuery animation duration value
1059 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1060 * to scroll in both directions
1061 * @param {Function} [config.complete] Function to call when scrolling completes.
1062 * Deprecated since 0.15.4, use the return promise instead.
1063 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1065 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1066 var position
, animations
, callback
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1067 deferred
= $.Deferred();
1069 // Configuration initialization
1070 config
= config
|| {};
1073 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1074 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1075 $container
= $( container
);
1076 elementDimensions
= this.getDimensions( el
);
1077 containerDimensions
= this.getDimensions( container
);
1078 $window
= $( this.getWindow( el
) );
1080 // Compute the element's position relative to the container
1081 if ( $container
.is( 'html, body' ) ) {
1082 // If the scrollable container is the root, this is easy
1084 top
: elementDimensions
.rect
.top
,
1085 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1086 left
: elementDimensions
.rect
.left
,
1087 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1090 // Otherwise, we have to subtract el's coordinates from container's coordinates
1092 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1093 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1094 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1095 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1099 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1100 if ( position
.top
< 0 ) {
1101 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1102 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1103 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1106 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1107 if ( position
.left
< 0 ) {
1108 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1109 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1110 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1113 if ( !$.isEmptyObject( animations
) ) {
1114 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1115 $container
.queue( function ( next
) {
1128 return deferred
.promise();
1132 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1133 * and reserve space for them, because it probably doesn't.
1135 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1136 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1137 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1138 * and then reattach (or show) them back.
1141 * @param {HTMLElement} el Element to reconsider the scrollbars on
1143 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1144 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1145 // Save scroll position
1146 scrollLeft
= el
.scrollLeft
;
1147 scrollTop
= el
.scrollTop
;
1148 // Detach all children
1149 while ( el
.firstChild
) {
1150 nodes
.push( el
.firstChild
);
1151 el
.removeChild( el
.firstChild
);
1154 void el
.offsetHeight
;
1155 // Reattach all children
1156 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1157 el
.appendChild( nodes
[ i
] );
1159 // Restore scroll position (no-op if scrollbars disappeared)
1160 el
.scrollLeft
= scrollLeft
;
1161 el
.scrollTop
= scrollTop
;
1167 * Toggle visibility of an element.
1169 * @param {boolean} [show] Make element visible, omit to toggle visibility
1173 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1174 show
= show
=== undefined ? !this.visible
: !!show
;
1176 if ( show
!== this.isVisible() ) {
1177 this.visible
= show
;
1178 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1179 this.emit( 'toggle', show
);
1186 * Check if element is visible.
1188 * @return {boolean} element is visible
1190 OO
.ui
.Element
.prototype.isVisible = function () {
1191 return this.visible
;
1197 * @return {Mixed} Element data
1199 OO
.ui
.Element
.prototype.getData = function () {
1206 * @param {Mixed} Element data
1209 OO
.ui
.Element
.prototype.setData = function ( data
) {
1215 * Check if element supports one or more methods.
1217 * @param {string|string[]} methods Method or list of methods to check
1218 * @return {boolean} All methods are supported
1220 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1224 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1225 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1226 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1231 return methods
.length
=== support
;
1235 * Update the theme-provided classes.
1237 * @localdoc This is called in element mixins and widget classes any time state changes.
1238 * Updating is debounced, minimizing overhead of changing multiple attributes and
1239 * guaranteeing that theme updates do not occur within an element's constructor
1241 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1242 this.debouncedUpdateThemeClassesHandler();
1247 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1248 * make them synchronous.
1250 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1251 OO
.ui
.theme
.updateElementClasses( this );
1255 * Get the HTML tag name.
1257 * Override this method to base the result on instance information.
1259 * @return {string} HTML tag name
1261 OO
.ui
.Element
.prototype.getTagName = function () {
1262 return this.constructor.static.tagName
;
1266 * Check if the element is attached to the DOM
1267 * @return {boolean} The element is attached to the DOM
1269 OO
.ui
.Element
.prototype.isElementAttached = function () {
1270 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1274 * Get the DOM document.
1276 * @return {HTMLDocument} Document object
1278 OO
.ui
.Element
.prototype.getElementDocument = function () {
1279 // Don't cache this in other ways either because subclasses could can change this.$element
1280 return OO
.ui
.Element
.static.getDocument( this.$element
);
1284 * Get the DOM window.
1286 * @return {Window} Window object
1288 OO
.ui
.Element
.prototype.getElementWindow = function () {
1289 return OO
.ui
.Element
.static.getWindow( this.$element
);
1293 * Get closest scrollable container.
1295 * @return {HTMLElement} Closest scrollable container
1297 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1298 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1302 * Get group element is in.
1304 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1306 OO
.ui
.Element
.prototype.getElementGroup = function () {
1307 return this.elementGroup
;
1311 * Set group element is in.
1313 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1316 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1317 this.elementGroup
= group
;
1322 * Scroll element into view.
1324 * @param {Object} [config] Configuration options
1325 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1327 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1328 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1332 * Restore the pre-infusion dynamic state for this widget.
1334 * This method is called after #$element has been inserted into DOM. The parameter is the return
1335 * value of #gatherPreInfuseState.
1338 * @param {Object} state
1340 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1344 * Wraps an HTML snippet for use with configuration values which default
1345 * to strings. This bypasses the default html-escaping done to string
1351 * @param {string} [content] HTML content
1353 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1355 this.content
= content
;
1360 OO
.initClass( OO
.ui
.HtmlSnippet
);
1367 * @return {string} Unchanged HTML snippet.
1369 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1370 return this.content
;
1374 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1375 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1376 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1377 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1378 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1382 * @extends OO.ui.Element
1383 * @mixins OO.EventEmitter
1386 * @param {Object} [config] Configuration options
1388 OO
.ui
.Layout
= function OoUiLayout( config
) {
1389 // Configuration initialization
1390 config
= config
|| {};
1392 // Parent constructor
1393 OO
.ui
.Layout
.parent
.call( this, config
);
1395 // Mixin constructors
1396 OO
.EventEmitter
.call( this );
1399 this.$element
.addClass( 'oo-ui-layout' );
1404 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1405 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1408 * Widgets are compositions of one or more OOjs UI elements that users can both view
1409 * and interact with. All widgets can be configured and modified via a standard API,
1410 * and their state can change dynamically according to a model.
1414 * @extends OO.ui.Element
1415 * @mixins OO.EventEmitter
1418 * @param {Object} [config] Configuration options
1419 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1420 * appearance reflects this state.
1422 OO
.ui
.Widget
= function OoUiWidget( config
) {
1423 // Initialize config
1424 config
= $.extend( { disabled
: false }, config
);
1426 // Parent constructor
1427 OO
.ui
.Widget
.parent
.call( this, config
);
1429 // Mixin constructors
1430 OO
.EventEmitter
.call( this );
1433 this.disabled
= null;
1434 this.wasDisabled
= null;
1437 this.$element
.addClass( 'oo-ui-widget' );
1438 this.setDisabled( !!config
.disabled
);
1443 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1444 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1446 /* Static Properties */
1449 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1450 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1455 * @property {boolean}
1457 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1464 * A 'disable' event is emitted when the disabled state of the widget changes
1465 * (i.e. on disable **and** enable).
1467 * @param {boolean} disabled Widget is disabled
1473 * A 'toggle' event is emitted when the visibility of the widget changes.
1475 * @param {boolean} visible Widget is visible
1481 * Check if the widget is disabled.
1483 * @return {boolean} Widget is disabled
1485 OO
.ui
.Widget
.prototype.isDisabled = function () {
1486 return this.disabled
;
1490 * Set the 'disabled' state of the widget.
1492 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1494 * @param {boolean} disabled Disable widget
1497 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1500 this.disabled
= !!disabled
;
1501 isDisabled
= this.isDisabled();
1502 if ( isDisabled
!== this.wasDisabled
) {
1503 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1504 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1505 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1506 this.emit( 'disable', isDisabled
);
1507 this.updateThemeClasses();
1509 this.wasDisabled
= isDisabled
;
1515 * Update the disabled state, in case of changes in parent widget.
1519 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1520 this.setDisabled( this.disabled
);
1531 * @param {Object} [config] Configuration options
1533 OO
.ui
.Theme
= function OoUiTheme( config
) {
1534 // Configuration initialization
1535 config
= config
|| {};
1540 OO
.initClass( OO
.ui
.Theme
);
1545 * Get a list of classes to be applied to a widget.
1547 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1548 * otherwise state transitions will not work properly.
1550 * @param {OO.ui.Element} element Element for which to get classes
1551 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1553 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1554 return { on
: [], off
: [] };
1558 * Update CSS classes provided by the theme.
1560 * For elements with theme logic hooks, this should be called any time there's a state change.
1562 * @param {OO.ui.Element} element Element for which to update classes
1563 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1565 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1566 var $elements
= $( [] ),
1567 classes
= this.getElementClasses( element
);
1569 if ( element
.$icon
) {
1570 $elements
= $elements
.add( element
.$icon
);
1572 if ( element
.$indicator
) {
1573 $elements
= $elements
.add( element
.$indicator
);
1577 .removeClass( classes
.off
.join( ' ' ) )
1578 .addClass( classes
.on
.join( ' ' ) );
1582 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1583 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1584 * order in which users will navigate through the focusable elements via the "tab" key.
1587 * // TabIndexedElement is mixed into the ButtonWidget class
1588 * // to provide a tabIndex property.
1589 * var button1 = new OO.ui.ButtonWidget( {
1593 * var button2 = new OO.ui.ButtonWidget( {
1597 * var button3 = new OO.ui.ButtonWidget( {
1601 * var button4 = new OO.ui.ButtonWidget( {
1605 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1611 * @param {Object} [config] Configuration options
1612 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1613 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1614 * functionality will be applied to it instead.
1615 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1616 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1617 * to remove the element from the tab-navigation flow.
1619 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1620 // Configuration initialization
1621 config
= $.extend( { tabIndex
: 0 }, config
);
1624 this.$tabIndexed
= null;
1625 this.tabIndex
= null;
1628 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1631 this.setTabIndex( config
.tabIndex
);
1632 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1637 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1642 * Set the element that should use the tabindex functionality.
1644 * This method is used to retarget a tabindex mixin so that its functionality applies
1645 * to the specified element. If an element is currently using the functionality, the mixin’s
1646 * effect on that element is removed before the new element is set up.
1648 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1651 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1652 var tabIndex
= this.tabIndex
;
1653 // Remove attributes from old $tabIndexed
1654 this.setTabIndex( null );
1655 // Force update of new $tabIndexed
1656 this.$tabIndexed
= $tabIndexed
;
1657 this.tabIndex
= tabIndex
;
1658 return this.updateTabIndex();
1662 * Set the value of the tabindex.
1664 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1667 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1668 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1670 if ( this.tabIndex
!== tabIndex
) {
1671 this.tabIndex
= tabIndex
;
1672 this.updateTabIndex();
1679 * Update the `tabindex` attribute, in case of changes to tab index or
1685 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1686 if ( this.$tabIndexed
) {
1687 if ( this.tabIndex
!== null ) {
1688 // Do not index over disabled elements
1689 this.$tabIndexed
.attr( {
1690 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1691 // Support: ChromeVox and NVDA
1692 // These do not seem to inherit aria-disabled from parent elements
1693 'aria-disabled': this.isDisabled().toString()
1696 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1703 * Handle disable events.
1706 * @param {boolean} disabled Element is disabled
1708 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1709 this.updateTabIndex();
1713 * Get the value of the tabindex.
1715 * @return {number|null} Tabindex value
1717 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1718 return this.tabIndex
;
1722 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1723 * interface element that can be configured with access keys for accessibility.
1724 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1726 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1731 * @param {Object} [config] Configuration options
1732 * @cfg {jQuery} [$button] The button element created by the class.
1733 * If this configuration is omitted, the button element will use a generated `<a>`.
1734 * @cfg {boolean} [framed=true] Render the button with a frame
1736 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1737 // Configuration initialization
1738 config
= config
|| {};
1741 this.$button
= null;
1743 this.active
= false;
1744 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1745 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1746 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1747 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1748 this.onClickHandler
= this.onClick
.bind( this );
1749 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1752 this.$element
.addClass( 'oo-ui-buttonElement' );
1753 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1754 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1759 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1761 /* Static Properties */
1764 * Cancel mouse down events.
1766 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1767 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1768 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1773 * @property {boolean}
1775 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1780 * A 'click' event is emitted when the button element is clicked.
1788 * Set the button element.
1790 * This method is used to retarget a button mixin so that its functionality applies to
1791 * the specified button element instead of the one created by the class. If a button element
1792 * is already set, the method will remove the mixin’s effect on that element.
1794 * @param {jQuery} $button Element to use as button
1796 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
1797 if ( this.$button
) {
1799 .removeClass( 'oo-ui-buttonElement-button' )
1800 .removeAttr( 'role accesskey' )
1802 mousedown
: this.onMouseDownHandler
,
1803 keydown
: this.onKeyDownHandler
,
1804 click
: this.onClickHandler
,
1805 keypress
: this.onKeyPressHandler
1809 this.$button
= $button
1810 .addClass( 'oo-ui-buttonElement-button' )
1811 .attr( { role
: 'button' } )
1813 mousedown
: this.onMouseDownHandler
,
1814 keydown
: this.onKeyDownHandler
,
1815 click
: this.onClickHandler
,
1816 keypress
: this.onKeyPressHandler
1821 * Handles mouse down events.
1824 * @param {jQuery.Event} e Mouse down event
1826 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
1827 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1830 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1831 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1832 // reliably remove the pressed class
1833 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
1834 // Prevent change of focus unless specifically configured otherwise
1835 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
1841 * Handles mouse up events.
1844 * @param {MouseEvent} e Mouse up event
1846 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
1847 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1850 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1851 // Stop listening for mouseup, since we only needed this once
1852 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
1856 * Handles mouse click events.
1859 * @param {jQuery.Event} e Mouse click event
1862 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
1863 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
1864 if ( this.emit( 'click' ) ) {
1871 * Handles key down events.
1874 * @param {jQuery.Event} e Key down event
1876 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
1877 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1880 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1881 // Run the keyup handler no matter where the key is when the button is let go, so we can
1882 // reliably remove the pressed class
1883 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
1887 * Handles key up events.
1890 * @param {KeyboardEvent} e Key up event
1892 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
1893 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1896 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1897 // Stop listening for keyup, since we only needed this once
1898 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
1902 * Handles key press events.
1905 * @param {jQuery.Event} e Key press event
1908 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
1909 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
1910 if ( this.emit( 'click' ) ) {
1917 * Check if button has a frame.
1919 * @return {boolean} Button is framed
1921 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
1926 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1928 * @param {boolean} [framed] Make button framed, omit to toggle
1931 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
1932 framed
= framed
=== undefined ? !this.framed
: !!framed
;
1933 if ( framed
!== this.framed
) {
1934 this.framed
= framed
;
1936 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
1937 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
1938 this.updateThemeClasses();
1945 * Set the button's active state.
1947 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
1948 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
1949 * for other button types.
1951 * @param {boolean} value Make button active
1954 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
1955 this.active
= !!value
;
1956 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
1961 * Check if the button is active
1963 * @return {boolean} The button is active
1965 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
1970 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
1971 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
1972 * items from the group is done through the interface the class provides.
1973 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1975 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
1981 * @param {Object} [config] Configuration options
1982 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
1983 * is omitted, the group element will use a generated `<div>`.
1985 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
1986 // Configuration initialization
1987 config
= config
|| {};
1992 this.aggregateItemEvents
= {};
1995 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2001 * Set the group element.
2003 * If an element is already set, items will be moved to the new element.
2005 * @param {jQuery} $group Element to use as group
2007 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2010 this.$group
= $group
;
2011 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2012 this.$group
.append( this.items
[ i
].$element
);
2017 * Check if a group contains no items.
2019 * @return {boolean} Group is empty
2021 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
2022 return !this.items
.length
;
2026 * Get all items in the group.
2028 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2029 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2032 * @return {OO.ui.Element[]} An array of items.
2034 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
2035 return this.items
.slice( 0 );
2039 * Get an item by its data.
2041 * Only the first item with matching data will be returned. To return all matching items,
2042 * use the #getItemsFromData method.
2044 * @param {Object} data Item data to search for
2045 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2047 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2049 hash
= OO
.getHash( data
);
2051 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2052 item
= this.items
[ i
];
2053 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2062 * Get items by their data.
2064 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2066 * @param {Object} data Item data to search for
2067 * @return {OO.ui.Element[]} Items with equivalent data
2069 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2071 hash
= OO
.getHash( data
),
2074 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2075 item
= this.items
[ i
];
2076 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2085 * Aggregate the events emitted by the group.
2087 * When events are aggregated, the group will listen to all contained items for the event,
2088 * and then emit the event under a new name. The new event will contain an additional leading
2089 * parameter containing the item that emitted the original event. Other arguments emitted from
2090 * the original event are passed through.
2092 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2093 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2094 * A `null` value will remove aggregated events.
2096 * @throws {Error} An error is thrown if aggregation already exists.
2098 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
2099 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
2101 for ( itemEvent
in events
) {
2102 groupEvent
= events
[ itemEvent
];
2104 // Remove existing aggregated event
2105 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2106 // Don't allow duplicate aggregations
2108 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
2110 // Remove event aggregation from existing items
2111 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2112 item
= this.items
[ i
];
2113 if ( item
.connect
&& item
.disconnect
) {
2115 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2116 item
.disconnect( this, remove
);
2119 // Prevent future items from aggregating event
2120 delete this.aggregateItemEvents
[ itemEvent
];
2123 // Add new aggregate event
2125 // Make future items aggregate event
2126 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
2127 // Add event aggregation to existing items
2128 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2129 item
= this.items
[ i
];
2130 if ( item
.connect
&& item
.disconnect
) {
2132 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
2133 item
.connect( this, add
);
2141 * Add items to the group.
2143 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2144 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2146 * @param {OO.ui.Element[]} items An array of items to add to the group
2147 * @param {number} [index] Index of the insertion point
2150 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2151 var i
, len
, item
, event
, events
, currentIndex
,
2154 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2157 // Check if item exists then remove it first, effectively "moving" it
2158 currentIndex
= this.items
.indexOf( item
);
2159 if ( currentIndex
>= 0 ) {
2160 this.removeItems( [ item
] );
2161 // Adjust index to compensate for removal
2162 if ( currentIndex
< index
) {
2167 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2169 for ( event
in this.aggregateItemEvents
) {
2170 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
2172 item
.connect( this, events
);
2174 item
.setElementGroup( this );
2175 itemElements
.push( item
.$element
.get( 0 ) );
2178 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2179 this.$group
.append( itemElements
);
2180 this.items
.push
.apply( this.items
, items
);
2181 } else if ( index
=== 0 ) {
2182 this.$group
.prepend( itemElements
);
2183 this.items
.unshift
.apply( this.items
, items
);
2185 this.items
[ index
].$element
.before( itemElements
);
2186 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
2193 * Remove the specified items from a group.
2195 * Removed items are detached (not removed) from the DOM so that they may be reused.
2196 * To remove all items from a group, you may wish to use the #clearItems method instead.
2198 * @param {OO.ui.Element[]} items An array of items to remove
2201 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2202 var i
, len
, item
, index
, remove
, itemEvent
;
2204 // Remove specific items
2205 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2207 index
= this.items
.indexOf( item
);
2208 if ( index
!== -1 ) {
2210 item
.connect
&& item
.disconnect
&&
2211 !$.isEmptyObject( this.aggregateItemEvents
)
2214 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2215 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2217 item
.disconnect( this, remove
);
2219 item
.setElementGroup( null );
2220 this.items
.splice( index
, 1 );
2221 item
.$element
.detach();
2229 * Clear all items from the group.
2231 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2232 * To remove only a subset of items from a group, use the #removeItems method.
2236 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2237 var i
, len
, item
, remove
, itemEvent
;
2240 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2241 item
= this.items
[ i
];
2243 item
.connect
&& item
.disconnect
&&
2244 !$.isEmptyObject( this.aggregateItemEvents
)
2247 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2248 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2250 item
.disconnect( this, remove
);
2252 item
.setElementGroup( null );
2253 item
.$element
.detach();
2261 * IconElement is often mixed into other classes to generate an icon.
2262 * Icons are graphics, about the size of normal text. They are used to aid the user
2263 * in locating a control or to convey information in a space-efficient way. See the
2264 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2265 * included in the library.
2267 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2273 * @param {Object} [config] Configuration options
2274 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2275 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2276 * the icon element be set to an existing icon instead of the one generated by this class, set a
2277 * value using a jQuery selection. For example:
2279 * // Use a <div> tag instead of a <span>
2281 * // Use an existing icon element instead of the one generated by the class
2282 * $icon: this.$element
2283 * // Use an icon element from a child widget
2284 * $icon: this.childwidget.$element
2285 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2286 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2287 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2288 * by the user's language.
2290 * Example of an i18n map:
2292 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2293 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2294 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2295 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2296 * text. The icon title is displayed when users move the mouse over the icon.
2298 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2299 // Configuration initialization
2300 config
= config
|| {};
2305 this.iconTitle
= null;
2308 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2309 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2310 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2315 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2317 /* Static Properties */
2320 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2321 * for i18n purposes and contains a `default` icon name and additional names keyed by
2322 * language code. The `default` name is used when no icon is keyed by the user's language.
2324 * Example of an i18n map:
2326 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2328 * Note: the static property will be overridden if the #icon configuration is used.
2332 * @property {Object|string}
2334 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2337 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2338 * function that returns title text, or `null` for no title.
2340 * The static property will be overridden if the #iconTitle configuration is used.
2344 * @property {string|Function|null}
2346 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2351 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2352 * applies to the specified icon element instead of the one created by the class. If an icon
2353 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2354 * and mixin methods will no longer affect the element.
2356 * @param {jQuery} $icon Element to use as icon
2358 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2361 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2362 .removeAttr( 'title' );
2366 .addClass( 'oo-ui-iconElement-icon' )
2367 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2368 if ( this.iconTitle
!== null ) {
2369 this.$icon
.attr( 'title', this.iconTitle
);
2372 this.updateThemeClasses();
2376 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2377 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2380 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2381 * by language code, or `null` to remove the icon.
2384 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2385 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2386 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2388 if ( this.icon
!== icon
) {
2390 if ( this.icon
!== null ) {
2391 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2393 if ( icon
!== null ) {
2394 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2400 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2401 this.updateThemeClasses();
2407 * Set the icon title. Use `null` to remove the title.
2409 * @param {string|Function|null} iconTitle A text string used as the icon title,
2410 * a function that returns title text, or `null` for no title.
2413 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2414 iconTitle
= typeof iconTitle
=== 'function' ||
2415 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2416 OO
.ui
.resolveMsg( iconTitle
) : null;
2418 if ( this.iconTitle
!== iconTitle
) {
2419 this.iconTitle
= iconTitle
;
2421 if ( this.iconTitle
!== null ) {
2422 this.$icon
.attr( 'title', iconTitle
);
2424 this.$icon
.removeAttr( 'title' );
2433 * Get the symbolic name of the icon.
2435 * @return {string} Icon name
2437 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2442 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2444 * @return {string} Icon title text
2446 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2447 return this.iconTitle
;
2451 * IndicatorElement is often mixed into other classes to generate an indicator.
2452 * Indicators are small graphics that are generally used in two ways:
2454 * - To draw attention to the status of an item. For example, an indicator might be
2455 * used to show that an item in a list has errors that need to be resolved.
2456 * - To clarify the function of a control that acts in an exceptional way (a button
2457 * that opens a menu instead of performing an action directly, for example).
2459 * For a list of indicators included in the library, please see the
2460 * [OOjs UI documentation on MediaWiki] [1].
2462 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2468 * @param {Object} [config] Configuration options
2469 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2470 * configuration is omitted, the indicator element will use a generated `<span>`.
2471 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2472 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2474 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2475 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2476 * or a function that returns title text. The indicator title is displayed when users move
2477 * the mouse over the indicator.
2479 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2480 // Configuration initialization
2481 config
= config
|| {};
2484 this.$indicator
= null;
2485 this.indicator
= null;
2486 this.indicatorTitle
= null;
2489 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2490 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2491 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2496 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2498 /* Static Properties */
2501 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2502 * The static property will be overridden if the #indicator configuration is used.
2506 * @property {string|null}
2508 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2511 * A text string used as the indicator title, a function that returns title text, or `null`
2512 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2516 * @property {string|Function|null}
2518 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2523 * Set the indicator element.
2525 * If an element is already set, it will be cleaned up before setting up the new element.
2527 * @param {jQuery} $indicator Element to use as indicator
2529 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2530 if ( this.$indicator
) {
2532 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2533 .removeAttr( 'title' );
2536 this.$indicator
= $indicator
2537 .addClass( 'oo-ui-indicatorElement-indicator' )
2538 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2539 if ( this.indicatorTitle
!== null ) {
2540 this.$indicator
.attr( 'title', this.indicatorTitle
);
2543 this.updateThemeClasses();
2547 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2549 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2552 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2553 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2555 if ( this.indicator
!== indicator
) {
2556 if ( this.$indicator
) {
2557 if ( this.indicator
!== null ) {
2558 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2560 if ( indicator
!== null ) {
2561 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2564 this.indicator
= indicator
;
2567 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2568 this.updateThemeClasses();
2574 * Set the indicator title.
2576 * The title is displayed when a user moves the mouse over the indicator.
2578 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
2579 * `null` for no indicator title
2582 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2583 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2584 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2585 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2587 if ( this.indicatorTitle
!== indicatorTitle
) {
2588 this.indicatorTitle
= indicatorTitle
;
2589 if ( this.$indicator
) {
2590 if ( this.indicatorTitle
!== null ) {
2591 this.$indicator
.attr( 'title', indicatorTitle
);
2593 this.$indicator
.removeAttr( 'title' );
2602 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2604 * @return {string} Symbolic name of indicator
2606 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2607 return this.indicator
;
2611 * Get the indicator title.
2613 * The title is displayed when a user moves the mouse over the indicator.
2615 * @return {string} Indicator title text
2617 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2618 return this.indicatorTitle
;
2622 * LabelElement is often mixed into other classes to generate a label, which
2623 * helps identify the function of an interface element.
2624 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2626 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2632 * @param {Object} [config] Configuration options
2633 * @cfg {jQuery} [$label] The label element created by the class. If this
2634 * configuration is omitted, the label element will use a generated `<span>`.
2635 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2636 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2637 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2638 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2639 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
2640 * The label will be truncated to fit if necessary.
2642 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2643 // Configuration initialization
2644 config
= config
|| {};
2649 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
2652 this.setLabel( config
.label
|| this.constructor.static.label
);
2653 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2658 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2663 * @event labelChange
2664 * @param {string} value
2667 /* Static Properties */
2670 * The label text. The label can be specified as a plaintext string, a function that will
2671 * produce a string in the future, or `null` for no label. The static value will
2672 * be overridden if a label is specified with the #label config option.
2676 * @property {string|Function|null}
2678 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2683 * Set the label element.
2685 * If an element is already set, it will be cleaned up before setting up the new element.
2687 * @param {jQuery} $label Element to use as label
2689 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2690 if ( this.$label
) {
2691 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2694 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2695 this.setLabelContent( this.label
);
2701 * An empty string will result in the label being hidden. A string containing only whitespace will
2702 * be converted to a single ` `.
2704 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2705 * text; or null for no label
2708 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2709 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2710 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
2712 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
2714 if ( this.label
!== label
) {
2715 if ( this.$label
) {
2716 this.setLabelContent( label
);
2719 this.emit( 'labelChange' );
2728 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2729 * text; or null for no label
2731 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2740 OO
.ui
.mixin
.LabelElement
.prototype.fitLabel = function () {
2741 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
2742 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
2749 * Set the content of the label.
2751 * Do not call this method until after the label element has been set by #setLabelElement.
2754 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2755 * text; or null for no label
2757 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2758 if ( typeof label
=== 'string' ) {
2759 if ( label
.match( /^\s*$/ ) ) {
2760 // Convert whitespace only string to a single non-breaking space
2761 this.$label
.html( ' ' );
2763 this.$label
.text( label
);
2765 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2766 this.$label
.html( label
.toString() );
2767 } else if ( label
instanceof jQuery
) {
2768 this.$label
.empty().append( label
);
2770 this.$label
.empty();
2775 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2776 * additional functionality to an element created by another class. The class provides
2777 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2778 * which are used to customize the look and feel of a widget to better describe its
2779 * importance and functionality.
2781 * The library currently contains the following styling flags for general use:
2783 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2784 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2785 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2787 * The flags affect the appearance of the buttons:
2790 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2791 * var button1 = new OO.ui.ButtonWidget( {
2792 * label: 'Constructive',
2793 * flags: 'constructive'
2795 * var button2 = new OO.ui.ButtonWidget( {
2796 * label: 'Destructive',
2797 * flags: 'destructive'
2799 * var button3 = new OO.ui.ButtonWidget( {
2800 * label: 'Progressive',
2801 * flags: 'progressive'
2803 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2805 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2806 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2808 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2814 * @param {Object} [config] Configuration options
2815 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2816 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2817 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2818 * @cfg {jQuery} [$flagged] The flagged element. By default,
2819 * the flagged functionality is applied to the element created by the class ($element).
2820 * If a different element is specified, the flagged functionality will be applied to it instead.
2822 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
2823 // Configuration initialization
2824 config
= config
|| {};
2828 this.$flagged
= null;
2831 this.setFlags( config
.flags
);
2832 this.setFlaggedElement( config
.$flagged
|| this.$element
);
2839 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2840 * parameter contains the name of each modified flag and indicates whether it was
2843 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2844 * that the flag was added, `false` that the flag was removed.
2850 * Set the flagged element.
2852 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2853 * If an element is already set, the method will remove the mixin’s effect on that element.
2855 * @param {jQuery} $flagged Element that should be flagged
2857 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
2858 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
2859 return 'oo-ui-flaggedElement-' + flag
;
2862 if ( this.$flagged
) {
2863 this.$flagged
.removeClass( classNames
);
2866 this.$flagged
= $flagged
.addClass( classNames
);
2870 * Check if the specified flag is set.
2872 * @param {string} flag Name of flag
2873 * @return {boolean} The flag is set
2875 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
2876 // This may be called before the constructor, thus before this.flags is set
2877 return this.flags
&& ( flag
in this.flags
);
2881 * Get the names of all flags set.
2883 * @return {string[]} Flag names
2885 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
2886 // This may be called before the constructor, thus before this.flags is set
2887 return Object
.keys( this.flags
|| {} );
2896 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
2897 var flag
, className
,
2900 classPrefix
= 'oo-ui-flaggedElement-';
2902 for ( flag
in this.flags
) {
2903 className
= classPrefix
+ flag
;
2904 changes
[ flag
] = false;
2905 delete this.flags
[ flag
];
2906 remove
.push( className
);
2909 if ( this.$flagged
) {
2910 this.$flagged
.removeClass( remove
.join( ' ' ) );
2913 this.updateThemeClasses();
2914 this.emit( 'flag', changes
);
2920 * Add one or more flags.
2922 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
2923 * or an object keyed by flag name with a boolean value that indicates whether the flag should
2924 * be added (`true`) or removed (`false`).
2928 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
2929 var i
, len
, flag
, className
,
2933 classPrefix
= 'oo-ui-flaggedElement-';
2935 if ( typeof flags
=== 'string' ) {
2936 className
= classPrefix
+ flags
;
2938 if ( !this.flags
[ flags
] ) {
2939 this.flags
[ flags
] = true;
2940 add
.push( className
);
2942 } else if ( Array
.isArray( flags
) ) {
2943 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
2945 className
= classPrefix
+ flag
;
2947 if ( !this.flags
[ flag
] ) {
2948 changes
[ flag
] = true;
2949 this.flags
[ flag
] = true;
2950 add
.push( className
);
2953 } else if ( OO
.isPlainObject( flags
) ) {
2954 for ( flag
in flags
) {
2955 className
= classPrefix
+ flag
;
2956 if ( flags
[ flag
] ) {
2958 if ( !this.flags
[ flag
] ) {
2959 changes
[ flag
] = true;
2960 this.flags
[ flag
] = true;
2961 add
.push( className
);
2965 if ( this.flags
[ flag
] ) {
2966 changes
[ flag
] = false;
2967 delete this.flags
[ flag
];
2968 remove
.push( className
);
2974 if ( this.$flagged
) {
2976 .addClass( add
.join( ' ' ) )
2977 .removeClass( remove
.join( ' ' ) );
2980 this.updateThemeClasses();
2981 this.emit( 'flag', changes
);
2987 * TitledElement is mixed into other classes to provide a `title` attribute.
2988 * Titles are rendered by the browser and are made visible when the user moves
2989 * the mouse over the element. Titles are not visible on touch devices.
2992 * // TitledElement provides a 'title' attribute to the
2993 * // ButtonWidget class
2994 * var button = new OO.ui.ButtonWidget( {
2995 * label: 'Button with Title',
2996 * title: 'I am a button'
2998 * $( 'body' ).append( button.$element );
3004 * @param {Object} [config] Configuration options
3005 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3006 * If this config is omitted, the title functionality is applied to $element, the
3007 * element created by the class.
3008 * @cfg {string|Function} [title] The title text or a function that returns text. If
3009 * this config is omitted, the value of the {@link #static-title static title} property is used.
3011 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3012 // Configuration initialization
3013 config
= config
|| {};
3016 this.$titled
= null;
3020 this.setTitle( config
.title
|| this.constructor.static.title
);
3021 this.setTitledElement( config
.$titled
|| this.$element
);
3026 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3028 /* Static Properties */
3031 * The title text, a function that returns text, or `null` for no title. The value of the static property
3032 * is overridden if the #title config option is used.
3036 * @property {string|Function|null}
3038 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3043 * Set the titled element.
3045 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3046 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3048 * @param {jQuery} $titled Element that should use the 'titled' functionality
3050 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3051 if ( this.$titled
) {
3052 this.$titled
.removeAttr( 'title' );
3055 this.$titled
= $titled
;
3057 this.$titled
.attr( 'title', this.title
);
3064 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3067 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3068 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3069 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3071 if ( this.title
!== title
) {
3072 if ( this.$titled
) {
3073 if ( title
!== null ) {
3074 this.$titled
.attr( 'title', title
);
3076 this.$titled
.removeAttr( 'title' );
3088 * @return {string} Title string
3090 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3095 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3096 * Accesskeys allow an user to go to a specific element by using
3097 * a shortcut combination of a browser specific keys + the key
3101 * // AccessKeyedElement provides an 'accesskey' attribute to the
3102 * // ButtonWidget class
3103 * var button = new OO.ui.ButtonWidget( {
3104 * label: 'Button with Accesskey',
3107 * $( 'body' ).append( button.$element );
3113 * @param {Object} [config] Configuration options
3114 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3115 * If this config is omitted, the accesskey functionality is applied to $element, the
3116 * element created by the class.
3117 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3118 * this config is omitted, no accesskey will be added.
3120 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3121 // Configuration initialization
3122 config
= config
|| {};
3125 this.$accessKeyed
= null;
3126 this.accessKey
= null;
3129 this.setAccessKey( config
.accessKey
|| null );
3130 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3135 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3137 /* Static Properties */
3140 * The access key, a function that returns a key, or `null` for no accesskey.
3144 * @property {string|Function|null}
3146 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3151 * Set the accesskeyed element.
3153 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3154 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3156 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3158 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3159 if ( this.$accessKeyed
) {
3160 this.$accessKeyed
.removeAttr( 'accesskey' );
3163 this.$accessKeyed
= $accessKeyed
;
3164 if ( this.accessKey
) {
3165 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3172 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
3175 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3176 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3178 if ( this.accessKey
!== accessKey
) {
3179 if ( this.$accessKeyed
) {
3180 if ( accessKey
!== null ) {
3181 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3183 this.$accessKeyed
.removeAttr( 'accesskey' );
3186 this.accessKey
= accessKey
;
3195 * @return {string} accessKey string
3197 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3198 return this.accessKey
;
3202 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3203 * feels, and functionality can be customized via the class’s configuration options
3204 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3207 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3210 * // A button widget
3211 * var button = new OO.ui.ButtonWidget( {
3212 * label: 'Button with Icon',
3214 * iconTitle: 'Remove'
3216 * $( 'body' ).append( button.$element );
3218 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3221 * @extends OO.ui.Widget
3222 * @mixins OO.ui.mixin.ButtonElement
3223 * @mixins OO.ui.mixin.IconElement
3224 * @mixins OO.ui.mixin.IndicatorElement
3225 * @mixins OO.ui.mixin.LabelElement
3226 * @mixins OO.ui.mixin.TitledElement
3227 * @mixins OO.ui.mixin.FlaggedElement
3228 * @mixins OO.ui.mixin.TabIndexedElement
3229 * @mixins OO.ui.mixin.AccessKeyedElement
3232 * @param {Object} [config] Configuration options
3233 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3234 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3235 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3237 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3238 // Configuration initialization
3239 config
= config
|| {};
3241 // Parent constructor
3242 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3244 // Mixin constructors
3245 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3246 OO
.ui
.mixin
.IconElement
.call( this, config
);
3247 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3248 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3249 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3250 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3251 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3252 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3257 this.noFollow
= false;
3260 this.connect( this, { disable
: 'onDisable' } );
3263 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3265 .addClass( 'oo-ui-buttonWidget' )
3266 .append( this.$button
);
3267 this.setHref( config
.href
);
3268 this.setTarget( config
.target
);
3269 this.setNoFollow( config
.noFollow
);
3274 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3275 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3276 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3277 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3278 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3279 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3280 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3281 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3282 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3289 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
3290 if ( !this.isDisabled() ) {
3291 // Remove the tab-index while the button is down to prevent the button from stealing focus
3292 this.$button
.removeAttr( 'tabindex' );
3295 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
3301 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
3302 if ( !this.isDisabled() ) {
3303 // Restore the tab-index after the button is up to restore the button's accessibility
3304 this.$button
.attr( 'tabindex', this.tabIndex
);
3307 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
3311 * Get hyperlink location.
3313 * @return {string} Hyperlink location
3315 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3320 * Get hyperlink target.
3322 * @return {string} Hyperlink target
3324 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3329 * Get search engine traversal hint.
3331 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3333 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3334 return this.noFollow
;
3338 * Set hyperlink location.
3340 * @param {string|null} href Hyperlink location, null to remove
3342 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3343 href
= typeof href
=== 'string' ? href
: null;
3344 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3348 if ( href
!== this.href
) {
3357 * Update the `href` attribute, in case of changes to href or
3363 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3364 if ( this.href
!== null && !this.isDisabled() ) {
3365 this.$button
.attr( 'href', this.href
);
3367 this.$button
.removeAttr( 'href' );
3374 * Handle disable events.
3377 * @param {boolean} disabled Element is disabled
3379 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3384 * Set hyperlink target.
3386 * @param {string|null} target Hyperlink target, null to remove
3388 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3389 target
= typeof target
=== 'string' ? target
: null;
3391 if ( target
!== this.target
) {
3392 this.target
= target
;
3393 if ( target
!== null ) {
3394 this.$button
.attr( 'target', target
);
3396 this.$button
.removeAttr( 'target' );
3404 * Set search engine traversal hint.
3406 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3408 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3409 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3411 if ( noFollow
!== this.noFollow
) {
3412 this.noFollow
= noFollow
;
3414 this.$button
.attr( 'rel', 'nofollow' );
3416 this.$button
.removeAttr( 'rel' );
3424 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3425 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3426 * removed, and cleared from the group.
3429 * // Example: A ButtonGroupWidget with two buttons
3430 * var button1 = new OO.ui.PopupButtonWidget( {
3431 * label: 'Select a category',
3434 * $content: $( '<p>List of categories...</p>' ),
3439 * var button2 = new OO.ui.ButtonWidget( {
3442 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3443 * items: [button1, button2]
3445 * $( 'body' ).append( buttonGroup.$element );
3448 * @extends OO.ui.Widget
3449 * @mixins OO.ui.mixin.GroupElement
3452 * @param {Object} [config] Configuration options
3453 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3455 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3456 // Configuration initialization
3457 config
= config
|| {};
3459 // Parent constructor
3460 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3462 // Mixin constructors
3463 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3466 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3467 if ( Array
.isArray( config
.items
) ) {
3468 this.addItems( config
.items
);
3474 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3475 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3478 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3479 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3480 * for a list of icons included in the library.
3483 * // An icon widget with a label
3484 * var myIcon = new OO.ui.IconWidget( {
3488 * // Create a label.
3489 * var iconLabel = new OO.ui.LabelWidget( {
3492 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3494 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3497 * @extends OO.ui.Widget
3498 * @mixins OO.ui.mixin.IconElement
3499 * @mixins OO.ui.mixin.TitledElement
3500 * @mixins OO.ui.mixin.FlaggedElement
3503 * @param {Object} [config] Configuration options
3505 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3506 // Configuration initialization
3507 config
= config
|| {};
3509 // Parent constructor
3510 OO
.ui
.IconWidget
.parent
.call( this, config
);
3512 // Mixin constructors
3513 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3514 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3515 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3518 this.$element
.addClass( 'oo-ui-iconWidget' );
3523 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3524 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3525 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3526 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3528 /* Static Properties */
3530 OO
.ui
.IconWidget
.static.tagName
= 'span';
3533 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3534 * attention to the status of an item or to clarify the function of a control. For a list of
3535 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3538 * // Example of an indicator widget
3539 * var indicator1 = new OO.ui.IndicatorWidget( {
3540 * indicator: 'alert'
3543 * // Create a fieldset layout to add a label
3544 * var fieldset = new OO.ui.FieldsetLayout();
3545 * fieldset.addItems( [
3546 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3548 * $( 'body' ).append( fieldset.$element );
3550 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3553 * @extends OO.ui.Widget
3554 * @mixins OO.ui.mixin.IndicatorElement
3555 * @mixins OO.ui.mixin.TitledElement
3558 * @param {Object} [config] Configuration options
3560 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3561 // Configuration initialization
3562 config
= config
|| {};
3564 // Parent constructor
3565 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3567 // Mixin constructors
3568 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3569 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3572 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3577 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3578 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3579 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3581 /* Static Properties */
3583 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3586 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3587 * be configured with a `label` option that is set to a string, a label node, or a function:
3589 * - String: a plaintext string
3590 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3591 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3592 * - Function: a function that will produce a string in the future. Functions are used
3593 * in cases where the value of the label is not currently defined.
3595 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3596 * will come into focus when the label is clicked.
3599 * // Examples of LabelWidgets
3600 * var label1 = new OO.ui.LabelWidget( {
3601 * label: 'plaintext label'
3603 * var label2 = new OO.ui.LabelWidget( {
3604 * label: $( '<a href="default.html">jQuery label</a>' )
3606 * // Create a fieldset layout with fields for each example
3607 * var fieldset = new OO.ui.FieldsetLayout();
3608 * fieldset.addItems( [
3609 * new OO.ui.FieldLayout( label1 ),
3610 * new OO.ui.FieldLayout( label2 )
3612 * $( 'body' ).append( fieldset.$element );
3615 * @extends OO.ui.Widget
3616 * @mixins OO.ui.mixin.LabelElement
3619 * @param {Object} [config] Configuration options
3620 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3621 * Clicking the label will focus the specified input field.
3623 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3624 // Configuration initialization
3625 config
= config
|| {};
3627 // Parent constructor
3628 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3630 // Mixin constructors
3631 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3632 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3635 this.input
= config
.input
;
3638 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3639 this.$element
.on( 'click', this.onClick
.bind( this ) );
3643 this.$element
.addClass( 'oo-ui-labelWidget' );
3648 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3649 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3650 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3652 /* Static Properties */
3654 OO
.ui
.LabelWidget
.static.tagName
= 'span';
3659 * Handles label mouse click events.
3662 * @param {jQuery.Event} e Mouse click event
3664 OO
.ui
.LabelWidget
.prototype.onClick = function () {
3665 this.input
.simulateLabelClick();
3670 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3671 * and that they should wait before proceeding. The pending state is visually represented with a pending
3672 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3673 * field of a {@link OO.ui.TextInputWidget text input widget}.
3675 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3676 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3677 * in process dialogs.
3680 * function MessageDialog( config ) {
3681 * MessageDialog.parent.call( this, config );
3683 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3685 * MessageDialog.static.actions = [
3686 * { action: 'save', label: 'Done', flags: 'primary' },
3687 * { label: 'Cancel', flags: 'safe' }
3690 * MessageDialog.prototype.initialize = function () {
3691 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3692 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3693 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
3694 * this.$body.append( this.content.$element );
3696 * MessageDialog.prototype.getBodyHeight = function () {
3699 * MessageDialog.prototype.getActionProcess = function ( action ) {
3700 * var dialog = this;
3701 * if ( action === 'save' ) {
3702 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3703 * return new OO.ui.Process()
3705 * .next( function () {
3706 * dialog.getActions().get({actions: 'save'})[0].popPending();
3709 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3712 * var windowManager = new OO.ui.WindowManager();
3713 * $( 'body' ).append( windowManager.$element );
3715 * var dialog = new MessageDialog();
3716 * windowManager.addWindows( [ dialog ] );
3717 * windowManager.openWindow( dialog );
3723 * @param {Object} [config] Configuration options
3724 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3726 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3727 // Configuration initialization
3728 config
= config
|| {};
3732 this.$pending
= null;
3735 this.setPendingElement( config
.$pending
|| this.$element
);
3740 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3745 * Set the pending element (and clean up any existing one).
3747 * @param {jQuery} $pending The element to set to pending.
3749 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3750 if ( this.$pending
) {
3751 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3754 this.$pending
= $pending
;
3755 if ( this.pending
> 0 ) {
3756 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3761 * Check if an element is pending.
3763 * @return {boolean} Element is pending
3765 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3766 return !!this.pending
;
3770 * Increase the pending counter. The pending state will remain active until the counter is zero
3771 * (i.e., the number of calls to #pushPending and #popPending is the same).
3775 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3776 if ( this.pending
=== 0 ) {
3777 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3778 this.updateThemeClasses();
3786 * Decrease the pending counter. The pending state will remain active until the counter is zero
3787 * (i.e., the number of calls to #pushPending and #popPending is the same).
3791 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3792 if ( this.pending
=== 1 ) {
3793 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3794 this.updateThemeClasses();
3796 this.pending
= Math
.max( 0, this.pending
- 1 );
3802 * Element that can be automatically clipped to visible boundaries.
3804 * Whenever the element's natural height changes, you have to call
3805 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3806 * clipping correctly.
3808 * The dimensions of #$clippableContainer will be compared to the boundaries of the
3809 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3810 * then #$clippable will be given a fixed reduced height and/or width and will be made
3811 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3812 * but you can build a static footer by setting #$clippableContainer to an element that contains
3813 * #$clippable and the footer.
3819 * @param {Object} [config] Configuration options
3820 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3821 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3822 * omit to use #$clippable
3824 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
3825 // Configuration initialization
3826 config
= config
|| {};
3829 this.$clippable
= null;
3830 this.$clippableContainer
= null;
3831 this.clipping
= false;
3832 this.clippedHorizontally
= false;
3833 this.clippedVertically
= false;
3834 this.$clippableScrollableContainer
= null;
3835 this.$clippableScroller
= null;
3836 this.$clippableWindow
= null;
3837 this.idealWidth
= null;
3838 this.idealHeight
= null;
3839 this.onClippableScrollHandler
= this.clip
.bind( this );
3840 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
3843 if ( config
.$clippableContainer
) {
3844 this.setClippableContainer( config
.$clippableContainer
);
3846 this.setClippableElement( config
.$clippable
|| this.$element
);
3852 * Set clippable element.
3854 * If an element is already set, it will be cleaned up before setting up the new element.
3856 * @param {jQuery} $clippable Element to make clippable
3858 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
3859 if ( this.$clippable
) {
3860 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
3861 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3862 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3865 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
3870 * Set clippable container.
3872 * This is the container that will be measured when deciding whether to clip. When clipping,
3873 * #$clippable will be resized in order to keep the clippable container fully visible.
3875 * If the clippable container is unset, #$clippable will be used.
3877 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3879 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
3880 this.$clippableContainer
= $clippableContainer
;
3881 if ( this.$clippable
) {
3889 * Do not turn clipping on until after the element is attached to the DOM and visible.
3891 * @param {boolean} [clipping] Enable clipping, omit to toggle
3894 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
3895 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
3897 if ( this.clipping
!== clipping
) {
3898 this.clipping
= clipping
;
3900 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
3901 // If the clippable container is the root, we have to listen to scroll events and check
3902 // jQuery.scrollTop on the window because of browser inconsistencies
3903 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
3904 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
3905 this.$clippableScrollableContainer
;
3906 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
3907 this.$clippableWindow
= $( this.getElementWindow() )
3908 .on( 'resize', this.onClippableWindowResizeHandler
);
3909 // Initial clip after visible
3912 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3913 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3915 this.$clippableScrollableContainer
= null;
3916 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
3917 this.$clippableScroller
= null;
3918 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
3919 this.$clippableWindow
= null;
3927 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
3929 * @return {boolean} Element will be clipped to the visible area
3931 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
3932 return this.clipping
;
3936 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
3938 * @return {boolean} Part of the element is being clipped
3940 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
3941 return this.clippedHorizontally
|| this.clippedVertically
;
3945 * Check if the right of the element is being clipped by the nearest scrollable container.
3947 * @return {boolean} Part of the element is being clipped
3949 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
3950 return this.clippedHorizontally
;
3954 * Check if the bottom of the element is being clipped by the nearest scrollable container.
3956 * @return {boolean} Part of the element is being clipped
3958 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
3959 return this.clippedVertically
;
3963 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
3965 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
3966 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
3968 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
3969 this.idealWidth
= width
;
3970 this.idealHeight
= height
;
3972 if ( !this.clipping
) {
3973 // Update dimensions
3974 this.$clippable
.css( { width
: width
, height
: height
} );
3976 // While clipping, idealWidth and idealHeight are not considered
3980 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
3981 * the element's natural height changes.
3983 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
3984 * overlapped by, the visible area of the nearest scrollable container.
3988 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
3989 var $container
, extraHeight
, extraWidth
, ccOffset
,
3990 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
3991 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
3992 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
3993 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
3994 buffer
= 7; // Chosen by fair dice roll
3996 if ( !this.clipping
) {
3997 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4001 $container
= this.$clippableContainer
|| this.$clippable
;
4002 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4003 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4004 ccOffset
= $container
.offset();
4005 $scrollableContainer
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4006 this.$clippableWindow
: this.$clippableScrollableContainer
;
4007 scOffset
= $scrollableContainer
.offset() || { top
: 0, left
: 0 };
4008 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4009 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4010 ccWidth
= $container
.outerWidth() + buffer
;
4011 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4012 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4013 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4014 desiredWidth
= ccOffset
.left
< 0 ?
4015 ccWidth
+ ccOffset
.left
:
4016 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4017 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4018 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4019 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4020 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4021 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4022 clipWidth
= allotedWidth
< naturalWidth
;
4023 clipHeight
= allotedHeight
< naturalHeight
;
4026 this.$clippable
.css( { overflowX
: 'scroll', width
: Math
.max( 0, allotedWidth
) } );
4028 this.$clippable
.css( { width
: this.idealWidth
? this.idealWidth
- extraWidth
: '', overflowX
: '' } );
4031 this.$clippable
.css( { overflowY
: 'scroll', height
: Math
.max( 0, allotedHeight
) } );
4033 this.$clippable
.css( { height
: this.idealHeight
? this.idealHeight
- extraHeight
: '', overflowY
: '' } );
4036 // If we stopped clipping in at least one of the dimensions
4037 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4038 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4041 this.clippedHorizontally
= clipWidth
;
4042 this.clippedVertically
= clipHeight
;
4048 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4049 * By default, each popup has an anchor that points toward its origin.
4050 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4053 * // A popup widget.
4054 * var popup = new OO.ui.PopupWidget( {
4055 * $content: $( '<p>Hi there!</p>' ),
4060 * $( 'body' ).append( popup.$element );
4061 * // To display the popup, toggle the visibility to 'true'.
4062 * popup.toggle( true );
4064 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4067 * @extends OO.ui.Widget
4068 * @mixins OO.ui.mixin.LabelElement
4069 * @mixins OO.ui.mixin.ClippableElement
4072 * @param {Object} [config] Configuration options
4073 * @cfg {number} [width=320] Width of popup in pixels
4074 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4075 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4076 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4077 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4078 * popup is leaning towards the right of the screen.
4079 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4080 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4081 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4082 * sentence in the given language.
4083 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4084 * See the [OOjs UI docs on MediaWiki][3] for an example.
4085 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4086 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4087 * @cfg {jQuery} [$content] Content to append to the popup's body
4088 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4089 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4090 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4091 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4093 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4094 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4096 * @cfg {boolean} [padded=false] Add padding to the popup's body
4098 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4099 // Configuration initialization
4100 config
= config
|| {};
4102 // Parent constructor
4103 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4105 // Properties (must be set before ClippableElement constructor call)
4106 this.$body
= $( '<div>' );
4107 this.$popup
= $( '<div>' );
4109 // Mixin constructors
4110 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4111 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4112 $clippable
: this.$body
,
4113 $clippableContainer
: this.$popup
4117 this.$anchor
= $( '<div>' );
4118 // If undefined, will be computed lazily in updateDimensions()
4119 this.$container
= config
.$container
;
4120 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4121 this.autoClose
= !!config
.autoClose
;
4122 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4123 this.transitionTimeout
= null;
4125 this.width
= config
.width
!== undefined ? config
.width
: 320;
4126 this.height
= config
.height
!== undefined ? config
.height
: null;
4127 this.setAlignment( config
.align
);
4128 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4129 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4132 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4133 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4134 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4136 .addClass( 'oo-ui-popupWidget-popup' )
4137 .append( this.$body
);
4139 .addClass( 'oo-ui-popupWidget' )
4140 .append( this.$popup
, this.$anchor
);
4141 // Move content, which was added to #$element by OO.ui.Widget, to the body
4142 // FIXME This is gross, we should use '$body' or something for the config
4143 if ( config
.$content
instanceof jQuery
) {
4144 this.$body
.append( config
.$content
);
4147 if ( config
.padded
) {
4148 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4151 if ( config
.head
) {
4152 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4153 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4154 this.$head
= $( '<div>' )
4155 .addClass( 'oo-ui-popupWidget-head' )
4156 .append( this.$label
, this.closeButton
.$element
);
4157 this.$popup
.prepend( this.$head
);
4160 if ( config
.$footer
) {
4161 this.$footer
= $( '<div>' )
4162 .addClass( 'oo-ui-popupWidget-footer' )
4163 .append( config
.$footer
);
4164 this.$popup
.append( this.$footer
);
4167 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4168 // that reference properties not initialized at that time of parent class construction
4169 // TODO: Find a better way to handle post-constructor setup
4170 this.visible
= false;
4171 this.$element
.addClass( 'oo-ui-element-hidden' );
4176 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4177 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4178 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4183 * Handles mouse down events.
4186 * @param {MouseEvent} e Mouse down event
4188 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4191 !$.contains( this.$element
[ 0 ], e
.target
) &&
4192 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
4194 this.toggle( false );
4199 * Bind mouse down listener.
4203 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4204 // Capture clicks outside popup
4205 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4209 * Handles close button click events.
4213 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4214 if ( this.isVisible() ) {
4215 this.toggle( false );
4220 * Unbind mouse down listener.
4224 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4225 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4229 * Handles key down events.
4232 * @param {KeyboardEvent} e Key down event
4234 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4236 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4239 this.toggle( false );
4241 e
.stopPropagation();
4246 * Bind key down listener.
4250 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4251 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4255 * Unbind key down listener.
4259 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4260 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4264 * Show, hide, or toggle the visibility of the anchor.
4266 * @param {boolean} [show] Show anchor, omit to toggle
4268 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4269 show
= show
=== undefined ? !this.anchored
: !!show
;
4271 if ( this.anchored
!== show
) {
4273 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4275 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4277 this.anchored
= show
;
4282 * Check if the anchor is visible.
4284 * @return {boolean} Anchor is visible
4286 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4293 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4295 show
= show
=== undefined ? !this.isVisible() : !!show
;
4297 change
= show
!== this.isVisible();
4300 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4304 if ( this.autoClose
) {
4305 this.bindMouseDownListener();
4306 this.bindKeyDownListener();
4308 this.updateDimensions();
4309 this.toggleClipping( true );
4311 this.toggleClipping( false );
4312 if ( this.autoClose
) {
4313 this.unbindMouseDownListener();
4314 this.unbindKeyDownListener();
4323 * Set the size of the popup.
4325 * Changing the size may also change the popup's position depending on the alignment.
4327 * @param {number} width Width in pixels
4328 * @param {number} height Height in pixels
4329 * @param {boolean} [transition=false] Use a smooth transition
4332 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4334 this.height
= height
!== undefined ? height
: null;
4335 if ( this.isVisible() ) {
4336 this.updateDimensions( transition
);
4341 * Update the size and position.
4343 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4344 * be called automatically.
4346 * @param {boolean} [transition=false] Use a smooth transition
4349 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4350 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4351 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
4355 if ( !this.$container
) {
4356 // Lazy-initialize $container if not specified in constructor
4357 this.$container
= $( this.getClosestScrollableElementContainer() );
4360 // Set height and width before measuring things, since it might cause our measurements
4361 // to change (e.g. due to scrollbars appearing or disappearing)
4364 height
: this.height
!== null ? this.height
: 'auto'
4367 // If we are in RTL, we need to flip the alignment, unless it is center
4368 if ( align
=== 'forwards' || align
=== 'backwards' ) {
4369 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
4370 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
4372 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
4377 // Compute initial popupOffset based on alignment
4378 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
4380 // Figure out if this will cause the popup to go beyond the edge of the container
4381 originOffset
= this.$element
.offset().left
;
4382 containerLeft
= this.$container
.offset().left
;
4383 containerWidth
= this.$container
.innerWidth();
4384 containerRight
= containerLeft
+ containerWidth
;
4385 popupLeft
= popupOffset
- this.containerPadding
;
4386 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4387 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4388 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4390 // Adjust offset to make the popup not go beyond the edge, if needed
4391 if ( overlapRight
< 0 ) {
4392 popupOffset
+= overlapRight
;
4393 } else if ( overlapLeft
< 0 ) {
4394 popupOffset
-= overlapLeft
;
4397 // Adjust offset to avoid anchor being rendered too close to the edge
4398 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4399 // TODO: Find a measurement that works for CSS anchors and image anchors
4400 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4401 if ( popupOffset
+ this.width
< anchorWidth
) {
4402 popupOffset
= anchorWidth
- this.width
;
4403 } else if ( -popupOffset
< anchorWidth
) {
4404 popupOffset
= -anchorWidth
;
4407 // Prevent transition from being interrupted
4408 clearTimeout( this.transitionTimeout
);
4410 // Enable transition
4411 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4414 // Position body relative to anchor
4415 this.$popup
.css( 'margin-left', popupOffset
);
4418 // Prevent transitioning after transition is complete
4419 this.transitionTimeout
= setTimeout( function () {
4420 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4423 // Prevent transitioning immediately
4424 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4427 // Reevaluate clipping state since we've relocated and resized the popup
4434 * Set popup alignment
4435 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4436 * `backwards` or `forwards`.
4438 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4439 // Validate alignment and transform deprecated values
4440 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4441 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
4443 this.align
= 'center';
4448 * Get popup alignment
4449 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4450 * `backwards` or `forwards`.
4452 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4457 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4458 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4459 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4460 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4466 * @param {Object} [config] Configuration options
4467 * @cfg {Object} [popup] Configuration to pass to popup
4468 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4470 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4471 // Configuration initialization
4472 config
= config
|| {};
4475 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4476 { autoClose
: true },
4478 { $autoCloseIgnore
: this.$element
}
4487 * @return {OO.ui.PopupWidget} Popup widget
4489 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4494 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4495 * which is used to display additional information or options.
4498 * // Example of a popup button.
4499 * var popupButton = new OO.ui.PopupButtonWidget( {
4500 * label: 'Popup button with options',
4503 * $content: $( '<p>Additional options here.</p>' ),
4505 * align: 'force-left'
4508 * // Append the button to the DOM.
4509 * $( 'body' ).append( popupButton.$element );
4512 * @extends OO.ui.ButtonWidget
4513 * @mixins OO.ui.mixin.PopupElement
4516 * @param {Object} [config] Configuration options
4518 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4519 // Parent constructor
4520 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4522 // Mixin constructors
4523 OO
.ui
.mixin
.PopupElement
.call( this, config
);
4526 this.connect( this, { click
: 'onAction' } );
4530 .addClass( 'oo-ui-popupButtonWidget' )
4531 .attr( 'aria-haspopup', 'true' )
4532 .append( this.popup
.$element
);
4537 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4538 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4543 * Handle the button action being triggered.
4547 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
4548 this.popup
.toggle();
4552 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4554 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4559 * @extends OO.ui.mixin.GroupElement
4562 * @param {Object} [config] Configuration options
4564 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
4565 // Parent constructor
4566 OO
.ui
.mixin
.GroupWidget
.parent
.call( this, config
);
4571 OO
.inheritClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
4576 * Set the disabled state of the widget.
4578 * This will also update the disabled state of child widgets.
4580 * @param {boolean} disabled Disable widget
4583 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
4587 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4588 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
4590 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4592 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4593 this.items
[ i
].updateDisabled();
4601 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4603 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4604 * allows bidirectional communication.
4606 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4614 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
4621 * Check if widget is disabled.
4623 * Checks parent if present, making disabled state inheritable.
4625 * @return {boolean} Widget is disabled
4627 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
4628 return this.disabled
||
4629 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
4633 * Set group element is in.
4635 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4638 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
4640 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4641 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
4643 // Initialize item disabled states
4644 this.updateDisabled();
4650 * OptionWidgets are special elements that can be selected and configured with data. The
4651 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4652 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4653 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4655 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4658 * @extends OO.ui.Widget
4659 * @mixins OO.ui.mixin.LabelElement
4660 * @mixins OO.ui.mixin.FlaggedElement
4663 * @param {Object} [config] Configuration options
4665 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
4666 // Configuration initialization
4667 config
= config
|| {};
4669 // Parent constructor
4670 OO
.ui
.OptionWidget
.parent
.call( this, config
);
4672 // Mixin constructors
4673 OO
.ui
.mixin
.ItemWidget
.call( this );
4674 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4675 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4678 this.selected
= false;
4679 this.highlighted
= false;
4680 this.pressed
= false;
4684 .data( 'oo-ui-optionWidget', this )
4685 .attr( 'role', 'option' )
4686 .attr( 'aria-selected', 'false' )
4687 .addClass( 'oo-ui-optionWidget' )
4688 .append( this.$label
);
4693 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
4694 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
4695 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
4696 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
4698 /* Static Properties */
4700 OO
.ui
.OptionWidget
.static.selectable
= true;
4702 OO
.ui
.OptionWidget
.static.highlightable
= true;
4704 OO
.ui
.OptionWidget
.static.pressable
= true;
4706 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
4711 * Check if the option can be selected.
4713 * @return {boolean} Item is selectable
4715 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
4716 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
4720 * Check if the option can be highlighted. A highlight indicates that the option
4721 * may be selected when a user presses enter or clicks. Disabled items cannot
4724 * @return {boolean} Item is highlightable
4726 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
4727 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
4731 * Check if the option can be pressed. The pressed state occurs when a user mouses
4732 * down on an item, but has not yet let go of the mouse.
4734 * @return {boolean} Item is pressable
4736 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
4737 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
4741 * Check if the option is selected.
4743 * @return {boolean} Item is selected
4745 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
4746 return this.selected
;
4750 * Check if the option is highlighted. A highlight indicates that the
4751 * item may be selected when a user presses enter or clicks.
4753 * @return {boolean} Item is highlighted
4755 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
4756 return this.highlighted
;
4760 * Check if the option is pressed. The pressed state occurs when a user mouses
4761 * down on an item, but has not yet let go of the mouse. The item may appear
4762 * selected, but it will not be selected until the user releases the mouse.
4764 * @return {boolean} Item is pressed
4766 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
4767 return this.pressed
;
4771 * Set the option’s selected state. In general, all modifications to the selection
4772 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4773 * method instead of this method.
4775 * @param {boolean} [state=false] Select option
4778 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
4779 if ( this.constructor.static.selectable
) {
4780 this.selected
= !!state
;
4782 .toggleClass( 'oo-ui-optionWidget-selected', state
)
4783 .attr( 'aria-selected', state
.toString() );
4784 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
4785 this.scrollElementIntoView();
4787 this.updateThemeClasses();
4793 * Set the option’s highlighted state. In general, all programmatic
4794 * modifications to the highlight should be handled by the
4795 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4796 * method instead of this method.
4798 * @param {boolean} [state=false] Highlight option
4801 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
4802 if ( this.constructor.static.highlightable
) {
4803 this.highlighted
= !!state
;
4804 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
4805 this.updateThemeClasses();
4811 * Set the option’s pressed state. In general, all
4812 * programmatic modifications to the pressed state should be handled by the
4813 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4814 * method instead of this method.
4816 * @param {boolean} [state=false] Press option
4819 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
4820 if ( this.constructor.static.pressable
) {
4821 this.pressed
= !!state
;
4822 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
4823 this.updateThemeClasses();
4829 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4830 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4831 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4834 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4835 * information, please see the [OOjs UI documentation on MediaWiki][1].
4838 * // Example of a select widget with three options
4839 * var select = new OO.ui.SelectWidget( {
4841 * new OO.ui.OptionWidget( {
4843 * label: 'Option One',
4845 * new OO.ui.OptionWidget( {
4847 * label: 'Option Two',
4849 * new OO.ui.OptionWidget( {
4851 * label: 'Option Three',
4855 * $( 'body' ).append( select.$element );
4857 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4861 * @extends OO.ui.Widget
4862 * @mixins OO.ui.mixin.GroupWidget
4865 * @param {Object} [config] Configuration options
4866 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4867 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4868 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4869 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4871 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
4872 // Configuration initialization
4873 config
= config
|| {};
4875 // Parent constructor
4876 OO
.ui
.SelectWidget
.parent
.call( this, config
);
4878 // Mixin constructors
4879 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
4882 this.pressed
= false;
4883 this.selecting
= null;
4884 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
4885 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
4886 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
4887 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
4888 this.keyPressBuffer
= '';
4889 this.keyPressBufferTimer
= null;
4890 this.blockMouseOverEvents
= 0;
4893 this.connect( this, {
4897 mousedown
: this.onMouseDown
.bind( this ),
4898 mouseover
: this.onMouseOver
.bind( this ),
4899 mouseleave
: this.onMouseLeave
.bind( this )
4904 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
4905 .attr( 'role', 'listbox' );
4906 if ( Array
.isArray( config
.items
) ) {
4907 this.addItems( config
.items
);
4913 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
4915 // Need to mixin base class as well
4916 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupElement
);
4917 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
4920 OO
.ui
.SelectWidget
.static.passAllFilter = function () {
4929 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
4931 * @param {OO.ui.OptionWidget|null} item Highlighted item
4937 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
4938 * pressed state of an option.
4940 * @param {OO.ui.OptionWidget|null} item Pressed item
4946 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
4948 * @param {OO.ui.OptionWidget|null} item Selected item
4953 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
4954 * @param {OO.ui.OptionWidget} item Chosen item
4960 * An `add` event is emitted when options are added to the select with the #addItems method.
4962 * @param {OO.ui.OptionWidget[]} items Added items
4963 * @param {number} index Index of insertion point
4969 * A `remove` event is emitted when options are removed from the select with the #clearItems
4970 * or #removeItems methods.
4972 * @param {OO.ui.OptionWidget[]} items Removed items
4978 * Handle mouse down events.
4981 * @param {jQuery.Event} e Mouse down event
4983 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
4986 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
4987 this.togglePressed( true );
4988 item
= this.getTargetItem( e
);
4989 if ( item
&& item
.isSelectable() ) {
4990 this.pressItem( item
);
4991 this.selecting
= item
;
4992 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
4993 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5000 * Handle mouse up events.
5003 * @param {MouseEvent} e Mouse up event
5005 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5008 this.togglePressed( false );
5009 if ( !this.selecting
) {
5010 item
= this.getTargetItem( e
);
5011 if ( item
&& item
.isSelectable() ) {
5012 this.selecting
= item
;
5015 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5016 this.pressItem( null );
5017 this.chooseItem( this.selecting
);
5018 this.selecting
= null;
5021 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5022 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5028 * Handle mouse move events.
5031 * @param {MouseEvent} e Mouse move event
5033 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5036 if ( !this.isDisabled() && this.pressed
) {
5037 item
= this.getTargetItem( e
);
5038 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5039 this.pressItem( item
);
5040 this.selecting
= item
;
5046 * Handle mouse over events.
5049 * @param {jQuery.Event} e Mouse over event
5051 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5053 if ( this.blockMouseOverEvents
) {
5056 if ( !this.isDisabled() ) {
5057 item
= this.getTargetItem( e
);
5058 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5064 * Handle mouse leave events.
5067 * @param {jQuery.Event} e Mouse over event
5069 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5070 if ( !this.isDisabled() ) {
5071 this.highlightItem( null );
5077 * Handle key down events.
5080 * @param {KeyboardEvent} e Key down event
5082 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5085 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5087 if ( !this.isDisabled() && this.isVisible() ) {
5088 switch ( e
.keyCode
) {
5089 case OO
.ui
.Keys
.ENTER
:
5090 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5091 // Was only highlighted, now let's select it. No-op if already selected.
5092 this.chooseItem( currentItem
);
5097 case OO
.ui
.Keys
.LEFT
:
5098 this.clearKeyPressBuffer();
5099 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5102 case OO
.ui
.Keys
.DOWN
:
5103 case OO
.ui
.Keys
.RIGHT
:
5104 this.clearKeyPressBuffer();
5105 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5108 case OO
.ui
.Keys
.ESCAPE
:
5109 case OO
.ui
.Keys
.TAB
:
5110 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5111 currentItem
.setHighlighted( false );
5113 this.unbindKeyDownListener();
5114 this.unbindKeyPressListener();
5115 // Don't prevent tabbing away / defocusing
5121 if ( nextItem
.constructor.static.highlightable
) {
5122 this.highlightItem( nextItem
);
5124 this.chooseItem( nextItem
);
5126 this.scrollItemIntoView( nextItem
);
5131 e
.stopPropagation();
5137 * Bind key down listener.
5141 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5142 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5146 * Unbind key down listener.
5150 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5151 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5155 * Scroll item into view, preventing spurious mouse highlight actions from happening.
5157 * @return {OO.ui.OptionWidget} Item to scroll into view
5159 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
5161 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5162 // and around 100-150 ms after it is finished.
5163 this.blockMouseOverEvents
++;
5164 item
.scrollElementIntoView().done( function () {
5165 setTimeout( function () {
5166 widget
.blockMouseOverEvents
--;
5172 * Clear the key-press buffer
5176 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5177 if ( this.keyPressBufferTimer
) {
5178 clearTimeout( this.keyPressBufferTimer
);
5179 this.keyPressBufferTimer
= null;
5181 this.keyPressBuffer
= '';
5185 * Handle key press events.
5188 * @param {KeyboardEvent} e Key press event
5190 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5191 var c
, filter
, item
;
5193 if ( !e
.charCode
) {
5194 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5195 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5200 if ( String
.fromCodePoint
) {
5201 c
= String
.fromCodePoint( e
.charCode
);
5203 c
= String
.fromCharCode( e
.charCode
);
5206 if ( this.keyPressBufferTimer
) {
5207 clearTimeout( this.keyPressBufferTimer
);
5209 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5211 item
= this.getHighlightedItem() || this.getSelectedItem();
5213 if ( this.keyPressBuffer
=== c
) {
5214 // Common (if weird) special case: typing "xxxx" will cycle through all
5215 // the items beginning with "x".
5217 item
= this.getRelativeSelectableItem( item
, 1 );
5220 this.keyPressBuffer
+= c
;
5223 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5224 if ( !item
|| !filter( item
) ) {
5225 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5228 if ( item
.constructor.static.highlightable
) {
5229 this.highlightItem( item
);
5231 this.chooseItem( item
);
5233 this.scrollItemIntoView( item
);
5237 e
.stopPropagation();
5241 * Get a matcher for the specific string
5244 * @param {string} s String to match against items
5245 * @param {boolean} [exact=false] Only accept exact matches
5246 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5248 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5251 if ( s
.normalize
) {
5254 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5255 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5259 re
= new RegExp( re
, 'i' );
5260 return function ( item
) {
5261 var l
= item
.getLabel();
5262 if ( typeof l
!== 'string' ) {
5263 l
= item
.$label
.text();
5265 if ( l
.normalize
) {
5268 return re
.test( l
);
5273 * Bind key press listener.
5277 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5278 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5282 * Unbind key down listener.
5284 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5289 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5290 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5291 this.clearKeyPressBuffer();
5295 * Visibility change handler
5298 * @param {boolean} visible
5300 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5302 this.clearKeyPressBuffer();
5307 * Get the closest item to a jQuery.Event.
5310 * @param {jQuery.Event} e
5311 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5313 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5314 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5318 * Get selected item.
5320 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5322 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5325 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5326 if ( this.items
[ i
].isSelected() ) {
5327 return this.items
[ i
];
5334 * Get highlighted item.
5336 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5338 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5341 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5342 if ( this.items
[ i
].isHighlighted() ) {
5343 return this.items
[ i
];
5350 * Toggle pressed state.
5352 * Press is a state that occurs when a user mouses down on an item, but
5353 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5354 * until the user releases the mouse.
5356 * @param {boolean} pressed An option is being pressed
5358 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5359 if ( pressed
=== undefined ) {
5360 pressed
= !this.pressed
;
5362 if ( pressed
!== this.pressed
) {
5364 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5365 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5366 this.pressed
= pressed
;
5371 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5372 * and any existing highlight will be removed. The highlight is mutually exclusive.
5374 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5378 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5379 var i
, len
, highlighted
,
5382 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5383 highlighted
= this.items
[ i
] === item
;
5384 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5385 this.items
[ i
].setHighlighted( highlighted
);
5390 this.emit( 'highlight', item
);
5397 * Fetch an item by its label.
5399 * @param {string} label Label of the item to select.
5400 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5401 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5403 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5405 len
= this.items
.length
,
5406 filter
= this.getItemMatcher( label
, true );
5408 for ( i
= 0; i
< len
; i
++ ) {
5409 item
= this.items
[ i
];
5410 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5417 filter
= this.getItemMatcher( label
, false );
5418 for ( i
= 0; i
< len
; i
++ ) {
5419 item
= this.items
[ i
];
5420 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5436 * Programmatically select an option by its label. If the item does not exist,
5437 * all options will be deselected.
5439 * @param {string} [label] Label of the item to select.
5440 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5444 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5445 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5446 if ( label
=== undefined || !itemFromLabel
) {
5447 return this.selectItem();
5449 return this.selectItem( itemFromLabel
);
5453 * Programmatically select an option by its data. If the `data` parameter is omitted,
5454 * or if the item does not exist, all options will be deselected.
5456 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5460 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5461 var itemFromData
= this.getItemFromData( data
);
5462 if ( data
=== undefined || !itemFromData
) {
5463 return this.selectItem();
5465 return this.selectItem( itemFromData
);
5469 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5470 * all options will be deselected.
5472 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5476 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
5477 var i
, len
, selected
,
5480 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5481 selected
= this.items
[ i
] === item
;
5482 if ( this.items
[ i
].isSelected() !== selected
) {
5483 this.items
[ i
].setSelected( selected
);
5488 this.emit( 'select', item
);
5497 * Press is a state that occurs when a user mouses down on an item, but has not
5498 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5499 * releases the mouse.
5501 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5505 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
5506 var i
, len
, pressed
,
5509 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5510 pressed
= this.items
[ i
] === item
;
5511 if ( this.items
[ i
].isPressed() !== pressed
) {
5512 this.items
[ i
].setPressed( pressed
);
5517 this.emit( 'press', item
);
5526 * Note that ‘choose’ should never be modified programmatically. A user can choose
5527 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5528 * use the #selectItem method.
5530 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5531 * when users choose an item with the keyboard or mouse.
5533 * @param {OO.ui.OptionWidget} item Item to choose
5537 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
5539 this.selectItem( item
);
5540 this.emit( 'choose', item
);
5547 * Get an option by its position relative to the specified item (or to the start of the option array,
5548 * if item is `null`). The direction in which to search through the option array is specified with a
5549 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5550 * `null` if there are no options in the array.
5552 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5553 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5554 * @param {Function} filter Only consider items for which this function returns
5555 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5556 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5558 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
5559 var currentIndex
, nextIndex
, i
,
5560 increase
= direction
> 0 ? 1 : -1,
5561 len
= this.items
.length
;
5563 if ( !$.isFunction( filter
) ) {
5564 filter
= OO
.ui
.SelectWidget
.static.passAllFilter
;
5567 if ( item
instanceof OO
.ui
.OptionWidget
) {
5568 currentIndex
= this.items
.indexOf( item
);
5569 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
5571 // If no item is selected and moving forward, start at the beginning.
5572 // If moving backward, start at the end.
5573 nextIndex
= direction
> 0 ? 0 : len
- 1;
5576 for ( i
= 0; i
< len
; i
++ ) {
5577 item
= this.items
[ nextIndex
];
5578 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5581 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
5587 * Get the next selectable item or `null` if there are no selectable items.
5588 * Disabled options and menu-section markers and breaks are not selectable.
5590 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5592 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
5595 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5596 item
= this.items
[ i
];
5597 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
5606 * Add an array of options to the select. Optionally, an index number can be used to
5607 * specify an insertion point.
5609 * @param {OO.ui.OptionWidget[]} items Items to add
5610 * @param {number} [index] Index to insert items after
5614 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
5616 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
5618 // Always provide an index, even if it was omitted
5619 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
5625 * Remove the specified array of options from the select. Options will be detached
5626 * from the DOM, not removed, so they can be reused later. To remove all options from
5627 * the select, you may wish to use the #clearItems method instead.
5629 * @param {OO.ui.OptionWidget[]} items Items to remove
5633 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
5636 // Deselect items being removed
5637 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5639 if ( item
.isSelected() ) {
5640 this.selectItem( null );
5645 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
5647 this.emit( 'remove', items
);
5653 * Clear all options from the select. Options will be detached from the DOM, not removed,
5654 * so that they can be reused later. To remove a subset of options from the select, use
5655 * the #removeItems method.
5660 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
5661 var items
= this.items
.slice();
5664 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
5667 this.selectItem( null );
5669 this.emit( 'remove', items
);
5675 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5676 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5677 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5678 * options. For more information about options and selects, please see the
5679 * [OOjs UI documentation on MediaWiki][1].
5682 * // Decorated options in a select widget
5683 * var select = new OO.ui.SelectWidget( {
5685 * new OO.ui.DecoratedOptionWidget( {
5687 * label: 'Option with icon',
5690 * new OO.ui.DecoratedOptionWidget( {
5692 * label: 'Option with indicator',
5697 * $( 'body' ).append( select.$element );
5699 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5702 * @extends OO.ui.OptionWidget
5703 * @mixins OO.ui.mixin.IconElement
5704 * @mixins OO.ui.mixin.IndicatorElement
5707 * @param {Object} [config] Configuration options
5709 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
5710 // Parent constructor
5711 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
5713 // Mixin constructors
5714 OO
.ui
.mixin
.IconElement
.call( this, config
);
5715 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
5719 .addClass( 'oo-ui-decoratedOptionWidget' )
5720 .prepend( this.$icon
)
5721 .append( this.$indicator
);
5726 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
5727 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
5728 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
5731 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5732 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5733 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5735 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5738 * @extends OO.ui.DecoratedOptionWidget
5741 * @param {Object} [config] Configuration options
5743 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
5744 // Configuration initialization
5745 config
= $.extend( { icon
: 'check' }, config
);
5747 // Parent constructor
5748 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
5752 .attr( 'role', 'menuitem' )
5753 .addClass( 'oo-ui-menuOptionWidget' );
5758 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5760 /* Static Properties */
5762 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
5765 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5766 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5769 * var myDropdown = new OO.ui.DropdownWidget( {
5772 * new OO.ui.MenuSectionOptionWidget( {
5775 * new OO.ui.MenuOptionWidget( {
5777 * label: 'Welsh Corgi'
5779 * new OO.ui.MenuOptionWidget( {
5781 * label: 'Standard Poodle'
5783 * new OO.ui.MenuSectionOptionWidget( {
5786 * new OO.ui.MenuOptionWidget( {
5793 * $( 'body' ).append( myDropdown.$element );
5796 * @extends OO.ui.DecoratedOptionWidget
5799 * @param {Object} [config] Configuration options
5801 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
5802 // Parent constructor
5803 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
5806 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
5811 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5813 /* Static Properties */
5815 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
5817 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
5820 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5821 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5822 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5823 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5824 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5825 * and customized to be opened, closed, and displayed as needed.
5827 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5828 * mouse outside the menu.
5830 * Menus also have support for keyboard interaction:
5832 * - Enter/Return key: choose and select a menu option
5833 * - Up-arrow key: highlight the previous menu option
5834 * - Down-arrow key: highlight the next menu option
5835 * - Esc key: hide the menu
5837 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5838 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5841 * @extends OO.ui.SelectWidget
5842 * @mixins OO.ui.mixin.ClippableElement
5845 * @param {Object} [config] Configuration options
5846 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5847 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5848 * and {@link OO.ui.mixin.LookupElement LookupElement}
5849 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5850 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
5851 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5852 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5853 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5854 * that button, unless the button (or its parent widget) is passed in here.
5855 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
5856 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
5858 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
5859 // Configuration initialization
5860 config
= config
|| {};
5862 // Parent constructor
5863 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
5865 // Mixin constructors
5866 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
5869 this.newItems
= null;
5870 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
5871 this.filterFromInput
= !!config
.filterFromInput
;
5872 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
5873 this.$widget
= config
.widget
? config
.widget
.$element
: null;
5874 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5875 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
5879 .addClass( 'oo-ui-menuSelectWidget' )
5880 .attr( 'role', 'menu' );
5882 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5883 // that reference properties not initialized at that time of parent class construction
5884 // TODO: Find a better way to handle post-constructor setup
5885 this.visible
= false;
5886 this.$element
.addClass( 'oo-ui-element-hidden' );
5891 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
5892 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
5897 * Handles document mouse down events.
5900 * @param {MouseEvent} e Mouse down event
5902 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
5904 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
5905 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
5907 this.toggle( false );
5914 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
5915 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5917 if ( !this.isDisabled() && this.isVisible() ) {
5918 switch ( e
.keyCode
) {
5919 case OO
.ui
.Keys
.LEFT
:
5920 case OO
.ui
.Keys
.RIGHT
:
5921 // Do nothing if a text field is associated, arrow keys will be handled natively
5922 if ( !this.$input
) {
5923 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5926 case OO
.ui
.Keys
.ESCAPE
:
5927 case OO
.ui
.Keys
.TAB
:
5928 if ( currentItem
) {
5929 currentItem
.setHighlighted( false );
5931 this.toggle( false );
5932 // Don't prevent tabbing away, prevent defocusing
5933 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
5935 e
.stopPropagation();
5939 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5946 * Update menu item visibility after input changes.
5949 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
5951 len
= this.items
.length
,
5952 showAll
= !this.isVisible(),
5953 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
5955 for ( i
= 0; i
< len
; i
++ ) {
5956 item
= this.items
[ i
];
5957 if ( item
instanceof OO
.ui
.OptionWidget
) {
5958 item
.toggle( showAll
|| filter( item
) );
5962 // Reevaluate clipping
5969 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
5970 if ( this.$input
) {
5971 this.$input
.on( 'keydown', this.onKeyDownHandler
);
5973 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
5980 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
5981 if ( this.$input
) {
5982 this.$input
.off( 'keydown', this.onKeyDownHandler
);
5984 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
5991 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
5992 if ( this.$input
) {
5993 if ( this.filterFromInput
) {
5994 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
5997 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6004 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6005 if ( this.$input
) {
6006 if ( this.filterFromInput
) {
6007 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6008 this.updateItemVisibility();
6011 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6018 * When a user chooses an item, the menu is closed.
6020 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6021 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6022 * @param {OO.ui.OptionWidget} item Item to choose
6025 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6026 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6027 this.toggle( false );
6034 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6038 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6041 if ( !this.newItems
) {
6045 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
6047 if ( this.isVisible() ) {
6048 // Defer fitting label until item has been attached
6051 this.newItems
.push( item
);
6055 // Reevaluate clipping
6064 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6066 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6068 // Reevaluate clipping
6077 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6079 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6081 // Reevaluate clipping
6090 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6093 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6094 change
= visible
!== this.isVisible();
6097 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6101 this.bindKeyDownListener();
6102 this.bindKeyPressListener();
6104 if ( this.newItems
&& this.newItems
.length
) {
6105 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
6106 this.newItems
[ i
].fitLabel();
6108 this.newItems
= null;
6110 this.toggleClipping( true );
6112 if ( this.getSelectedItem() ) {
6113 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
6117 if ( this.autoHide
) {
6118 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6121 this.unbindKeyDownListener();
6122 this.unbindKeyPressListener();
6123 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6124 this.toggleClipping( false );
6132 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6133 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6134 * users can interact with it.
6136 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6137 * OO.ui.DropdownInputWidget instead.
6140 * // Example: A DropdownWidget with a menu that contains three options
6141 * var dropDown = new OO.ui.DropdownWidget( {
6142 * label: 'Dropdown menu: Select a menu option',
6145 * new OO.ui.MenuOptionWidget( {
6149 * new OO.ui.MenuOptionWidget( {
6153 * new OO.ui.MenuOptionWidget( {
6161 * $( 'body' ).append( dropDown.$element );
6163 * dropDown.getMenu().selectItemByData( 'b' );
6165 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6167 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6169 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6172 * @extends OO.ui.Widget
6173 * @mixins OO.ui.mixin.IconElement
6174 * @mixins OO.ui.mixin.IndicatorElement
6175 * @mixins OO.ui.mixin.LabelElement
6176 * @mixins OO.ui.mixin.TitledElement
6177 * @mixins OO.ui.mixin.TabIndexedElement
6180 * @param {Object} [config] Configuration options
6181 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6182 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6183 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6184 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6186 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6187 // Configuration initialization
6188 config
= $.extend( { indicator
: 'down' }, config
);
6190 // Parent constructor
6191 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6193 // Properties (must be set before TabIndexedElement constructor call)
6194 this.$handle
= this.$( '<span>' );
6195 this.$overlay
= config
.$overlay
|| this.$element
;
6197 // Mixin constructors
6198 OO
.ui
.mixin
.IconElement
.call( this, config
);
6199 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6200 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6201 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6202 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6205 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6207 $container
: this.$element
6212 click
: this.onClick
.bind( this ),
6213 keydown
: this.onKeyDown
.bind( this )
6215 this.menu
.connect( this, { select
: 'onMenuSelect' } );
6219 .addClass( 'oo-ui-dropdownWidget-handle' )
6220 .append( this.$icon
, this.$label
, this.$indicator
);
6222 .addClass( 'oo-ui-dropdownWidget' )
6223 .append( this.$handle
);
6224 this.$overlay
.append( this.menu
.$element
);
6229 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6230 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6231 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6232 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6233 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6234 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6241 * @return {OO.ui.MenuSelectWidget} Menu of widget
6243 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6248 * Handles menu select events.
6251 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6253 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6257 this.setLabel( null );
6261 selectedLabel
= item
.getLabel();
6263 // If the label is a DOM element, clone it, because setLabel will append() it
6264 if ( selectedLabel
instanceof jQuery
) {
6265 selectedLabel
= selectedLabel
.clone();
6268 this.setLabel( selectedLabel
);
6272 * Handle mouse click events.
6275 * @param {jQuery.Event} e Mouse click event
6277 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6278 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6285 * Handle key down events.
6288 * @param {jQuery.Event} e Key down event
6290 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
6292 !this.isDisabled() &&
6294 e
.which
=== OO
.ui
.Keys
.ENTER
||
6296 !this.menu
.isVisible() &&
6298 e
.which
=== OO
.ui
.Keys
.SPACE
||
6299 e
.which
=== OO
.ui
.Keys
.UP
||
6300 e
.which
=== OO
.ui
.Keys
.DOWN
6311 * RadioOptionWidget is an option widget that looks like a radio button.
6312 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6313 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6315 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6318 * @extends OO.ui.OptionWidget
6321 * @param {Object} [config] Configuration options
6323 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6324 // Configuration initialization
6325 config
= config
|| {};
6327 // Properties (must be done before parent constructor which calls #setDisabled)
6328 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6330 // Parent constructor
6331 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6334 this.radio
.$input
.on( 'focus', this.onInputFocus
.bind( this ) );
6337 // Remove implicit role, we're handling it ourselves
6338 this.radio
.$input
.attr( 'role', 'presentation' );
6340 .addClass( 'oo-ui-radioOptionWidget' )
6341 .attr( 'role', 'radio' )
6342 .attr( 'aria-checked', 'false' )
6343 .removeAttr( 'aria-selected' )
6344 .prepend( this.radio
.$element
);
6349 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6351 /* Static Properties */
6353 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6355 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6357 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6359 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6364 * @param {jQuery.Event} e Focus event
6367 OO
.ui
.RadioOptionWidget
.prototype.onInputFocus = function () {
6368 this.radio
.$input
.blur();
6369 this.$element
.parent().focus();
6375 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6376 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6378 this.radio
.setSelected( state
);
6380 .attr( 'aria-checked', state
.toString() )
6381 .removeAttr( 'aria-selected' );
6389 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6390 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6392 this.radio
.setDisabled( this.isDisabled() );
6398 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6399 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6400 * an interface for adding, removing and selecting options.
6401 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6403 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6404 * OO.ui.RadioSelectInputWidget instead.
6407 * // A RadioSelectWidget with RadioOptions.
6408 * var option1 = new OO.ui.RadioOptionWidget( {
6410 * label: 'Selected radio option'
6413 * var option2 = new OO.ui.RadioOptionWidget( {
6415 * label: 'Unselected radio option'
6418 * var radioSelect=new OO.ui.RadioSelectWidget( {
6419 * items: [ option1, option2 ]
6422 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6423 * radioSelect.selectItem( option1 );
6425 * $( 'body' ).append( radioSelect.$element );
6427 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6431 * @extends OO.ui.SelectWidget
6432 * @mixins OO.ui.mixin.TabIndexedElement
6435 * @param {Object} [config] Configuration options
6437 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6438 // Parent constructor
6439 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6441 // Mixin constructors
6442 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6446 focus
: this.bindKeyDownListener
.bind( this ),
6447 blur
: this.unbindKeyDownListener
.bind( this )
6452 .addClass( 'oo-ui-radioSelectWidget' )
6453 .attr( 'role', 'radiogroup' );
6458 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6459 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6462 * Element that will stick under a specified container, even when it is inserted elsewhere in the
6463 * document (for example, in a OO.ui.Window's $overlay).
6465 * The elements's position is automatically calculated and maintained when window is resized or the
6466 * page is scrolled. If you reposition the container manually, you have to call #position to make
6467 * sure the element is still placed correctly.
6469 * As positioning is only possible when both the element and the container are attached to the DOM
6470 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
6471 * the #toggle method to display a floating popup, for example.
6477 * @param {Object} [config] Configuration options
6478 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
6479 * @cfg {jQuery} [$floatableContainer] Node to position below
6481 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
6482 // Configuration initialization
6483 config
= config
|| {};
6486 this.$floatable
= null;
6487 this.$floatableContainer
= null;
6488 this.$floatableWindow
= null;
6489 this.$floatableClosestScrollable
= null;
6490 this.onFloatableScrollHandler
= this.position
.bind( this );
6491 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
6494 this.setFloatableContainer( config
.$floatableContainer
);
6495 this.setFloatableElement( config
.$floatable
|| this.$element
);
6501 * Set floatable element.
6503 * If an element is already set, it will be cleaned up before setting up the new element.
6505 * @param {jQuery} $floatable Element to make floatable
6507 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
6508 if ( this.$floatable
) {
6509 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
6510 this.$floatable
.css( { left
: '', top
: '' } );
6513 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
6518 * Set floatable container.
6520 * The element will be always positioned under the specified container.
6522 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
6524 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
6525 this.$floatableContainer
= $floatableContainer
;
6526 if ( this.$floatable
) {
6532 * Toggle positioning.
6534 * Do not turn positioning on until after the element is attached to the DOM and visible.
6536 * @param {boolean} [positioning] Enable positioning, omit to toggle
6539 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
6540 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
6542 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
6544 if ( this.positioning
!== positioning
) {
6545 this.positioning
= positioning
;
6547 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
6548 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
6549 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6550 // If the scrollable is the root, we have to listen to scroll events
6551 // on the window because of browser inconsistencies (or do we? someone should verify this)
6552 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
6553 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
6557 if ( positioning
) {
6558 this.$floatableWindow
= $( this.getElementWindow() );
6559 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
6561 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6562 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
6563 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
6566 // Initial position after visible
6569 if ( this.$floatableWindow
) {
6570 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
6571 this.$floatableWindow
= null;
6574 if ( this.$floatableClosestScrollable
) {
6575 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
6576 this.$floatableClosestScrollable
= null;
6579 this.$floatable
.css( { left
: '', top
: '' } );
6587 * Position the floatable below its container.
6589 * This should only be done when both of them are attached to the DOM and visible.
6593 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
6596 if ( !this.positioning
) {
6600 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
6602 // Position under container
6603 pos
.top
+= this.$floatableContainer
.height();
6604 this.$floatable
.css( pos
);
6606 // We updated the position, so re-evaluate the clipping state.
6607 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
6608 // will not notice the need to update itself.)
6609 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
6610 // it not listen to the right events in the right places?
6619 * FloatingMenuSelectWidget is a menu that will stick under a specified
6620 * container, even when it is inserted elsewhere in the document (for example,
6621 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
6622 * menu from being clipped too aggresively.
6624 * The menu's position is automatically calculated and maintained when the menu
6625 * is toggled or the window is resized.
6627 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
6630 * @extends OO.ui.MenuSelectWidget
6631 * @mixins OO.ui.mixin.FloatableElement
6634 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
6635 * Deprecated, omit this parameter and specify `$container` instead.
6636 * @param {Object} [config] Configuration options
6637 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
6639 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
6640 // Allow 'inputWidget' parameter and config for backwards compatibility
6641 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
6642 config
= inputWidget
;
6643 inputWidget
= config
.inputWidget
;
6646 // Configuration initialization
6647 config
= config
|| {};
6649 // Parent constructor
6650 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
6652 // Properties (must be set before mixin constructors)
6653 this.inputWidget
= inputWidget
; // For backwards compatibility
6654 this.$container
= config
.$container
|| this.inputWidget
.$element
;
6656 // Mixins constructors
6657 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
6660 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
6661 // For backwards compatibility
6662 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
6667 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
6668 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
6670 // For backwards compatibility
6671 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
6678 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
6680 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
6681 change
= visible
!== this.isVisible();
6683 if ( change
&& visible
) {
6684 // Make sure the width is set before the parent method runs.
6685 this.setIdealSize( this.$container
.width() );
6689 // This will call this.clip(), which is nonsensical since we're not positioned yet...
6690 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6693 this.togglePositioning( this.isVisible() );
6700 * InputWidget is the base class for all input widgets, which
6701 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
6702 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
6703 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
6705 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
6709 * @extends OO.ui.Widget
6710 * @mixins OO.ui.mixin.FlaggedElement
6711 * @mixins OO.ui.mixin.TabIndexedElement
6712 * @mixins OO.ui.mixin.TitledElement
6713 * @mixins OO.ui.mixin.AccessKeyedElement
6716 * @param {Object} [config] Configuration options
6717 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
6718 * @cfg {string} [value=''] The value of the input.
6719 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
6720 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
6721 * before it is accepted.
6723 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
6724 // Configuration initialization
6725 config
= config
|| {};
6727 // Parent constructor
6728 OO
.ui
.InputWidget
.parent
.call( this, config
);
6731 this.$input
= this.getInputElement( config
);
6733 this.inputFilter
= config
.inputFilter
;
6735 // Mixin constructors
6736 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6737 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
6738 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
6739 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
6742 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
6746 .addClass( 'oo-ui-inputWidget-input' )
6747 .attr( 'name', config
.name
)
6748 .prop( 'disabled', this.isDisabled() );
6750 .addClass( 'oo-ui-inputWidget' )
6751 .append( this.$input
);
6752 this.setValue( config
.value
);
6754 this.setDir( config
.dir
);
6760 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
6761 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
6762 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6763 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
6764 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6766 /* Static Properties */
6768 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
6770 /* Static Methods */
6775 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
6776 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
6777 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
6778 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
6785 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
6786 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
6787 state
.value
= config
.$input
.val();
6788 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
6789 state
.focus
= config
.$input
.is( ':focus' );
6798 * A change event is emitted when the value of the input changes.
6800 * @param {string} value
6806 * Get input element.
6808 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
6809 * different circumstances. The element must have a `value` property (like form elements).
6812 * @param {Object} config Configuration options
6813 * @return {jQuery} Input element
6815 OO
.ui
.InputWidget
.prototype.getInputElement = function ( config
) {
6816 // See #reusePreInfuseDOM about config.$input
6817 return config
.$input
|| $( '<input>' );
6821 * Handle potentially value-changing events.
6824 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
6826 OO
.ui
.InputWidget
.prototype.onEdit = function () {
6828 if ( !this.isDisabled() ) {
6829 // Allow the stack to clear so the value will be updated
6830 setTimeout( function () {
6831 widget
.setValue( widget
.$input
.val() );
6837 * Get the value of the input.
6839 * @return {string} Input value
6841 OO
.ui
.InputWidget
.prototype.getValue = function () {
6842 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
6843 // it, and we won't know unless they're kind enough to trigger a 'change' event.
6844 var value
= this.$input
.val();
6845 if ( this.value
!== value
) {
6846 this.setValue( value
);
6852 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
6854 * @deprecated since v0.13.1, use #setDir directly
6855 * @param {boolean} isRTL Directionality is right-to-left
6858 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
6859 this.setDir( isRTL
? 'rtl' : 'ltr' );
6864 * Set the directionality of the input.
6866 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
6869 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
6870 this.$input
.prop( 'dir', dir
);
6875 * Set the value of the input.
6877 * @param {string} value New value
6881 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
6882 value
= this.cleanUpValue( value
);
6883 // Update the DOM if it has changed. Note that with cleanUpValue, it
6884 // is possible for the DOM value to change without this.value changing.
6885 if ( this.$input
.val() !== value
) {
6886 this.$input
.val( value
);
6888 if ( this.value
!== value
) {
6890 this.emit( 'change', this.value
);
6896 * Clean up incoming value.
6898 * Ensures value is a string, and converts undefined and null to empty string.
6901 * @param {string} value Original value
6902 * @return {string} Cleaned up value
6904 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
6905 if ( value
=== undefined || value
=== null ) {
6907 } else if ( this.inputFilter
) {
6908 return this.inputFilter( String( value
) );
6910 return String( value
);
6915 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
6916 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
6919 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
6920 if ( !this.isDisabled() ) {
6921 if ( this.$input
.is( ':checkbox, :radio' ) ) {
6922 this.$input
.click();
6924 if ( this.$input
.is( ':input' ) ) {
6925 this.$input
[ 0 ].focus();
6933 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
6934 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
6935 if ( this.$input
) {
6936 this.$input
.prop( 'disabled', this.isDisabled() );
6946 OO
.ui
.InputWidget
.prototype.focus = function () {
6947 this.$input
[ 0 ].focus();
6956 OO
.ui
.InputWidget
.prototype.blur = function () {
6957 this.$input
[ 0 ].blur();
6964 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
6965 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
6966 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
6967 this.setValue( state
.value
);
6969 if ( state
.focus
) {
6975 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
6976 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
6977 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
6978 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
6979 * [OOjs UI documentation on MediaWiki] [1] for more information.
6982 * // A ButtonInputWidget rendered as an HTML button, the default.
6983 * var button = new OO.ui.ButtonInputWidget( {
6984 * label: 'Input button',
6988 * $( 'body' ).append( button.$element );
6990 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
6993 * @extends OO.ui.InputWidget
6994 * @mixins OO.ui.mixin.ButtonElement
6995 * @mixins OO.ui.mixin.IconElement
6996 * @mixins OO.ui.mixin.IndicatorElement
6997 * @mixins OO.ui.mixin.LabelElement
6998 * @mixins OO.ui.mixin.TitledElement
7001 * @param {Object} [config] Configuration options
7002 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7003 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
7004 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
7005 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7006 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
7008 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
7009 // Configuration initialization
7010 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
7012 // Properties (must be set before parent constructor, which calls #setValue)
7013 this.useInputTag
= config
.useInputTag
;
7015 // Parent constructor
7016 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
7018 // Mixin constructors
7019 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
7020 OO
.ui
.mixin
.IconElement
.call( this, config
);
7021 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7022 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7023 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7026 if ( !config
.useInputTag
) {
7027 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
7029 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
7034 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
7035 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
7036 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
7037 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7038 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
7039 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7041 /* Static Properties */
7044 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7045 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7047 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
7055 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
7057 // See InputWidget#reusePreInfuseDOM about config.$input
7058 if ( config
.$input
) {
7059 return config
.$input
.empty();
7061 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
7062 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
7068 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
7070 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7071 * text, or `null` for no label
7074 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
7075 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
7077 if ( this.useInputTag
) {
7078 if ( typeof label
=== 'function' ) {
7079 label
= OO
.ui
.resolveMsg( label
);
7081 if ( label
instanceof jQuery
) {
7082 label
= label
.text();
7087 this.$input
.val( label
);
7094 * Set the value of the input.
7096 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
7097 * they do not support {@link #value values}.
7099 * @param {string} value New value
7102 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
7103 if ( !this.useInputTag
) {
7104 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
7110 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7111 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7112 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7113 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7115 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7118 * // An example of selected, unselected, and disabled checkbox inputs
7119 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7123 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7126 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7130 * // Create a fieldset layout with fields for each checkbox.
7131 * var fieldset = new OO.ui.FieldsetLayout( {
7132 * label: 'Checkboxes'
7134 * fieldset.addItems( [
7135 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7136 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7137 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7139 * $( 'body' ).append( fieldset.$element );
7141 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7144 * @extends OO.ui.InputWidget
7147 * @param {Object} [config] Configuration options
7148 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7150 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
7151 // Configuration initialization
7152 config
= config
|| {};
7154 // Parent constructor
7155 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
7159 .addClass( 'oo-ui-checkboxInputWidget' )
7160 // Required for pretty styling in MediaWiki theme
7161 .append( $( '<span>' ) );
7162 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7167 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
7169 /* Static Methods */
7174 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7175 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7176 state
.checked
= config
.$input
.prop( 'checked' );
7186 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
7187 return $( '<input type="checkbox" />' );
7193 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
7195 if ( !this.isDisabled() ) {
7196 // Allow the stack to clear so the value will be updated
7197 setTimeout( function () {
7198 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
7204 * Set selection state of this checkbox.
7206 * @param {boolean} state `true` for selected
7209 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
7211 if ( this.selected
!== state
) {
7212 this.selected
= state
;
7213 this.$input
.prop( 'checked', this.selected
);
7214 this.emit( 'change', this.selected
);
7220 * Check if this checkbox is selected.
7222 * @return {boolean} Checkbox is selected
7224 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
7225 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7226 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7227 var selected
= this.$input
.prop( 'checked' );
7228 if ( this.selected
!== selected
) {
7229 this.setSelected( selected
);
7231 return this.selected
;
7237 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7238 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7239 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7240 this.setSelected( state
.checked
);
7245 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7246 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7247 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7248 * more information about input widgets.
7250 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7251 * are no options. If no `value` configuration option is provided, the first option is selected.
7252 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7254 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7257 * // Example: A DropdownInputWidget with three options
7258 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7260 * { data: 'a', label: 'First' },
7261 * { data: 'b', label: 'Second'},
7262 * { data: 'c', label: 'Third' }
7265 * $( 'body' ).append( dropdownInput.$element );
7267 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7270 * @extends OO.ui.InputWidget
7271 * @mixins OO.ui.mixin.TitledElement
7274 * @param {Object} [config] Configuration options
7275 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7276 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7278 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
7279 // Configuration initialization
7280 config
= config
|| {};
7282 // Properties (must be done before parent constructor which calls #setDisabled)
7283 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
7285 // Parent constructor
7286 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
7288 // Mixin constructors
7289 OO
.ui
.mixin
.TitledElement
.call( this, config
);
7292 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
7295 this.setOptions( config
.options
|| [] );
7297 .addClass( 'oo-ui-dropdownInputWidget' )
7298 .append( this.dropdownWidget
.$element
);
7303 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
7304 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
7312 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function ( config
) {
7313 // See InputWidget#reusePreInfuseDOM about config.$input
7314 if ( config
.$input
) {
7315 return config
.$input
.addClass( 'oo-ui-element-hidden' );
7317 return $( '<input type="hidden">' );
7321 * Handles menu select events.
7324 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7326 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
7327 this.setValue( item
.getData() );
7333 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
7334 value
= this.cleanUpValue( value
);
7335 this.dropdownWidget
.getMenu().selectItemByData( value
);
7336 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
7343 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
7344 this.dropdownWidget
.setDisabled( state
);
7345 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7350 * Set the options available for this input.
7352 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7355 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
7357 value
= this.getValue(),
7360 // Rebuild the dropdown menu
7361 this.dropdownWidget
.getMenu()
7363 .addItems( options
.map( function ( opt
) {
7364 var optValue
= widget
.cleanUpValue( opt
.data
);
7365 return new OO
.ui
.MenuOptionWidget( {
7367 label
: opt
.label
!== undefined ? opt
.label
: optValue
7371 // Restore the previous value, or reset to something sensible
7372 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
7373 // Previous value is still available, ensure consistency with the dropdown
7374 this.setValue( value
);
7376 // No longer valid, reset
7377 if ( options
.length
) {
7378 this.setValue( options
[ 0 ].data
);
7388 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
7389 this.dropdownWidget
.getMenu().toggle( true );
7396 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
7397 this.dropdownWidget
.getMenu().toggle( false );
7402 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
7403 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
7404 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
7405 * please see the [OOjs UI documentation on MediaWiki][1].
7407 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7410 * // An example of selected, unselected, and disabled radio inputs
7411 * var radio1 = new OO.ui.RadioInputWidget( {
7415 * var radio2 = new OO.ui.RadioInputWidget( {
7418 * var radio3 = new OO.ui.RadioInputWidget( {
7422 * // Create a fieldset layout with fields for each radio button.
7423 * var fieldset = new OO.ui.FieldsetLayout( {
7424 * label: 'Radio inputs'
7426 * fieldset.addItems( [
7427 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
7428 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
7429 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
7431 * $( 'body' ).append( fieldset.$element );
7433 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7436 * @extends OO.ui.InputWidget
7439 * @param {Object} [config] Configuration options
7440 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
7442 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
7443 // Configuration initialization
7444 config
= config
|| {};
7446 // Parent constructor
7447 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
7451 .addClass( 'oo-ui-radioInputWidget' )
7452 // Required for pretty styling in MediaWiki theme
7453 .append( $( '<span>' ) );
7454 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7459 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
7461 /* Static Methods */
7466 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7467 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7468 state
.checked
= config
.$input
.prop( 'checked' );
7478 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
7479 return $( '<input type="radio" />' );
7485 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
7486 // RadioInputWidget doesn't track its state.
7490 * Set selection state of this radio button.
7492 * @param {boolean} state `true` for selected
7495 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
7496 // RadioInputWidget doesn't track its state.
7497 this.$input
.prop( 'checked', state
);
7502 * Check if this radio button is selected.
7504 * @return {boolean} Radio is selected
7506 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
7507 return this.$input
.prop( 'checked' );
7513 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7514 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7515 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7516 this.setSelected( state
.checked
);
7521 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
7522 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7523 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7524 * more information about input widgets.
7526 * This and OO.ui.DropdownInputWidget support the same configuration options.
7529 * // Example: A RadioSelectInputWidget with three options
7530 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
7532 * { data: 'a', label: 'First' },
7533 * { data: 'b', label: 'Second'},
7534 * { data: 'c', label: 'Third' }
7537 * $( 'body' ).append( radioSelectInput.$element );
7539 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7542 * @extends OO.ui.InputWidget
7545 * @param {Object} [config] Configuration options
7546 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7548 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
7549 // Configuration initialization
7550 config
= config
|| {};
7552 // Properties (must be done before parent constructor which calls #setDisabled)
7553 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
7555 // Parent constructor
7556 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
7559 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
7562 this.setOptions( config
.options
|| [] );
7564 .addClass( 'oo-ui-radioSelectInputWidget' )
7565 .append( this.radioSelectWidget
.$element
);
7570 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
7572 /* Static Properties */
7574 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
7576 /* Static Methods */
7581 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7582 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7583 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
7593 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
7594 return $( '<input type="hidden">' );
7598 * Handles menu select events.
7601 * @param {OO.ui.RadioOptionWidget} item Selected menu item
7603 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
7604 this.setValue( item
.getData() );
7610 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
7611 value
= this.cleanUpValue( value
);
7612 this.radioSelectWidget
.selectItemByData( value
);
7613 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
7620 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
7621 this.radioSelectWidget
.setDisabled( state
);
7622 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7627 * Set the options available for this input.
7629 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7632 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
7634 value
= this.getValue(),
7637 // Rebuild the radioSelect menu
7638 this.radioSelectWidget
7640 .addItems( options
.map( function ( opt
) {
7641 var optValue
= widget
.cleanUpValue( opt
.data
);
7642 return new OO
.ui
.RadioOptionWidget( {
7644 label
: opt
.label
!== undefined ? opt
.label
: optValue
7648 // Restore the previous value, or reset to something sensible
7649 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
7650 // Previous value is still available, ensure consistency with the radioSelect
7651 this.setValue( value
);
7653 // No longer valid, reset
7654 if ( options
.length
) {
7655 this.setValue( options
[ 0 ].data
);
7663 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
7664 * size of the field as well as its presentation. In addition, these widgets can be configured
7665 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
7666 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
7667 * which modifies incoming values rather than validating them.
7668 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7670 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7673 * // Example of a text input widget
7674 * var textInput = new OO.ui.TextInputWidget( {
7675 * value: 'Text input'
7677 * $( 'body' ).append( textInput.$element );
7679 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7682 * @extends OO.ui.InputWidget
7683 * @mixins OO.ui.mixin.IconElement
7684 * @mixins OO.ui.mixin.IndicatorElement
7685 * @mixins OO.ui.mixin.PendingElement
7686 * @mixins OO.ui.mixin.LabelElement
7689 * @param {Object} [config] Configuration options
7690 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
7691 * 'email' or 'url'. Ignored if `multiline` is true.
7693 * Some values of `type` result in additional behaviors:
7695 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
7696 * empties the text field
7697 * @cfg {string} [placeholder] Placeholder text
7698 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
7699 * instruct the browser to focus this widget.
7700 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
7701 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
7702 * @cfg {boolean} [multiline=false] Allow multiple lines of text
7703 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
7704 * specifies minimum number of rows to display.
7705 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
7706 * Use the #maxRows config to specify a maximum number of displayed rows.
7707 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
7708 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
7709 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
7710 * the value or placeholder text: `'before'` or `'after'`
7711 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
7712 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
7713 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
7714 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
7715 * (the value must contain only numbers); when RegExp, a regular expression that must match the
7716 * value for it to be considered valid; when Function, a function receiving the value as parameter
7717 * that must return true, or promise resolving to true, for it to be considered valid.
7719 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
7720 // Configuration initialization
7721 config
= $.extend( {
7723 labelPosition
: 'after'
7725 if ( config
.type
=== 'search' ) {
7726 if ( config
.icon
=== undefined ) {
7727 config
.icon
= 'search';
7729 // indicator: 'clear' is set dynamically later, depending on value
7731 if ( config
.required
) {
7732 if ( config
.indicator
=== undefined ) {
7733 config
.indicator
= 'required';
7737 // Parent constructor
7738 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
7740 // Mixin constructors
7741 OO
.ui
.mixin
.IconElement
.call( this, config
);
7742 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7743 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
7744 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7747 this.type
= this.getSaneType( config
);
7748 this.readOnly
= false;
7749 this.multiline
= !!config
.multiline
;
7750 this.autosize
= !!config
.autosize
;
7751 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
7752 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
7753 this.validate
= null;
7754 this.styleHeight
= null;
7755 this.scrollWidth
= null;
7757 // Clone for resizing
7758 if ( this.autosize
) {
7759 this.$clone
= this.$input
7761 .insertAfter( this.$input
)
7762 .attr( 'aria-hidden', 'true' )
7763 .addClass( 'oo-ui-element-hidden' );
7766 this.setValidation( config
.validate
);
7767 this.setLabelPosition( config
.labelPosition
);
7771 keypress
: this.onKeyPress
.bind( this ),
7772 blur
: this.onBlur
.bind( this )
7775 focus
: this.onElementAttach
.bind( this )
7777 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
7778 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
7779 this.on( 'labelChange', this.updatePosition
.bind( this ) );
7780 this.connect( this, {
7782 disable
: 'onDisable'
7787 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
7788 .append( this.$icon
, this.$indicator
);
7789 this.setReadOnly( !!config
.readOnly
);
7790 this.updateSearchIndicator();
7791 if ( config
.placeholder
) {
7792 this.$input
.attr( 'placeholder', config
.placeholder
);
7794 if ( config
.maxLength
!== undefined ) {
7795 this.$input
.attr( 'maxlength', config
.maxLength
);
7797 if ( config
.autofocus
) {
7798 this.$input
.attr( 'autofocus', 'autofocus' );
7800 if ( config
.required
) {
7801 this.$input
.attr( 'required', 'required' );
7802 this.$input
.attr( 'aria-required', 'true' );
7804 if ( config
.autocomplete
=== false ) {
7805 this.$input
.attr( 'autocomplete', 'off' );
7806 // Turning off autocompletion also disables "form caching" when the user navigates to a
7807 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
7809 beforeunload: function () {
7810 this.$input
.removeAttr( 'autocomplete' );
7812 pageshow: function () {
7813 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
7814 // whole page... it shouldn't hurt, though.
7815 this.$input
.attr( 'autocomplete', 'off' );
7819 if ( this.multiline
&& config
.rows
) {
7820 this.$input
.attr( 'rows', config
.rows
);
7822 if ( this.label
|| config
.autosize
) {
7823 this.installParentChangeDetector();
7829 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
7830 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
7831 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7832 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
7833 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
7835 /* Static Properties */
7837 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
7842 /* Static Methods */
7847 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7848 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7849 if ( config
.multiline
) {
7850 state
.scrollTop
= config
.$input
.scrollTop();
7858 * An `enter` event is emitted when the user presses 'enter' inside the text box.
7860 * Not emitted if the input is multiline.
7866 * A `resize` event is emitted when autosize is set and the widget resizes
7874 * Handle icon mouse down events.
7877 * @param {jQuery.Event} e Mouse down event
7880 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
7881 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7882 this.$input
[ 0 ].focus();
7888 * Handle indicator mouse down events.
7891 * @param {jQuery.Event} e Mouse down event
7894 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
7895 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7896 if ( this.type
=== 'search' ) {
7897 // Clear the text field
7898 this.setValue( '' );
7900 this.$input
[ 0 ].focus();
7906 * Handle key press events.
7909 * @param {jQuery.Event} e Key press event
7910 * @fires enter If enter key is pressed and input is not multiline
7912 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
7913 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
7914 this.emit( 'enter', e
);
7919 * Handle blur events.
7922 * @param {jQuery.Event} e Blur event
7924 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
7925 this.setValidityFlag();
7929 * Handle element attach events.
7932 * @param {jQuery.Event} e Element attach event
7934 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
7935 // Any previously calculated size is now probably invalid if we reattached elsewhere
7936 this.valCache
= null;
7938 this.positionLabel();
7942 * Handle change events.
7944 * @param {string} value
7947 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
7948 this.updateSearchIndicator();
7949 this.setValidityFlag();
7954 * Handle disable events.
7956 * @param {boolean} disabled Element is disabled
7959 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
7960 this.updateSearchIndicator();
7964 * Check if the input is {@link #readOnly read-only}.
7968 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
7969 return this.readOnly
;
7973 * Set the {@link #readOnly read-only} state of the input.
7975 * @param {boolean} state Make input read-only
7978 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
7979 this.readOnly
= !!state
;
7980 this.$input
.prop( 'readOnly', this.readOnly
);
7981 this.updateSearchIndicator();
7986 * Support function for making #onElementAttach work across browsers.
7988 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
7989 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
7991 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
7992 * first time that the element gets attached to the documented.
7994 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
7995 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
7996 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
7999 if ( MutationObserver
) {
8000 // The new way. If only it wasn't so ugly.
8002 if ( this.$element
.closest( 'html' ).length
) {
8003 // Widget is attached already, do nothing. This breaks the functionality of this function when
8004 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8005 // would require observation of the whole document, which would hurt performance of other,
8006 // more important code.
8010 // Find topmost node in the tree
8011 topmostNode
= this.$element
[ 0 ];
8012 while ( topmostNode
.parentNode
) {
8013 topmostNode
= topmostNode
.parentNode
;
8016 // We have no way to detect the $element being attached somewhere without observing the entire
8017 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8018 // parent node of $element, and instead detect when $element is removed from it (and thus
8019 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8020 // doesn't get attached, we end up back here and create the parent.
8022 mutationObserver
= new MutationObserver( function ( mutations
) {
8023 var i
, j
, removedNodes
;
8024 for ( i
= 0; i
< mutations
.length
; i
++ ) {
8025 removedNodes
= mutations
[ i
].removedNodes
;
8026 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
8027 if ( removedNodes
[ j
] === topmostNode
) {
8028 setTimeout( onRemove
, 0 );
8035 onRemove = function () {
8036 // If the node was attached somewhere else, report it
8037 if ( widget
.$element
.closest( 'html' ).length
) {
8038 widget
.onElementAttach();
8040 mutationObserver
.disconnect();
8041 widget
.installParentChangeDetector();
8044 // Create a fake parent and observe it
8045 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
8046 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
8048 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8049 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8050 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
8055 * Automatically adjust the size of the text input.
8057 * This only affects #multiline inputs that are {@link #autosize autosized}.
8062 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
8063 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
8064 idealHeight
, newHeight
, scrollWidth
, property
;
8066 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
8067 if ( this.autosize
) {
8069 .val( this.$input
.val() )
8070 .attr( 'rows', this.minRows
)
8071 // Set inline height property to 0 to measure scroll height
8072 .css( 'height', 0 );
8074 this.$clone
.removeClass( 'oo-ui-element-hidden' );
8076 this.valCache
= this.$input
.val();
8078 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
8080 // Remove inline height property to measure natural heights
8081 this.$clone
.css( 'height', '' );
8082 innerHeight
= this.$clone
.innerHeight();
8083 outerHeight
= this.$clone
.outerHeight();
8085 // Measure max rows height
8087 .attr( 'rows', this.maxRows
)
8088 .css( 'height', 'auto' )
8090 maxInnerHeight
= this.$clone
.innerHeight();
8092 // Difference between reported innerHeight and scrollHeight with no scrollbars present
8093 // Equals 1 on Blink-based browsers and 0 everywhere else
8094 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
8095 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
8097 this.$clone
.addClass( 'oo-ui-element-hidden' );
8099 // Only apply inline height when expansion beyond natural height is needed
8100 // Use the difference between the inner and outer height as a buffer
8101 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
8102 if ( newHeight
!== this.styleHeight
) {
8103 this.$input
.css( 'height', newHeight
);
8104 this.styleHeight
= newHeight
;
8105 this.emit( 'resize' );
8108 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
8109 if ( scrollWidth
!== this.scrollWidth
) {
8110 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
8112 this.$label
.css( { right
: '', left
: '' } );
8113 this.$indicator
.css( { right
: '', left
: '' } );
8115 if ( scrollWidth
) {
8116 this.$indicator
.css( property
, scrollWidth
);
8117 if ( this.labelPosition
=== 'after' ) {
8118 this.$label
.css( property
, scrollWidth
);
8122 this.scrollWidth
= scrollWidth
;
8123 this.positionLabel();
8133 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
8134 return config
.multiline
?
8136 $( '<input type="' + this.getSaneType( config
) + '" />' );
8140 * Get sanitized value for 'type' for given config.
8142 * @param {Object} config Configuration options
8143 * @return {string|null}
8146 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
8147 var type
= [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config
.type
) !== -1 ?
8150 return config
.multiline
? 'multiline' : type
;
8154 * Check if the input supports multiple lines.
8158 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
8159 return !!this.multiline
;
8163 * Check if the input automatically adjusts its size.
8167 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
8168 return !!this.autosize
;
8172 * Focus the input and select a specified range within the text.
8174 * @param {number} from Select from offset
8175 * @param {number} [to] Select to offset, defaults to from
8178 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
8179 var isBackwards
, start
, end
,
8180 input
= this.$input
[ 0 ];
8184 isBackwards
= to
< from;
8185 start
= isBackwards
? to
: from;
8186 end
= isBackwards
? from : to
;
8190 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
8195 * Get an object describing the current selection range in a directional manner
8197 * @return {Object} Object containing 'from' and 'to' offsets
8199 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
8200 var input
= this.$input
[ 0 ],
8201 start
= input
.selectionStart
,
8202 end
= input
.selectionEnd
,
8203 isBackwards
= input
.selectionDirection
=== 'backward';
8206 from: isBackwards
? end
: start
,
8207 to
: isBackwards
? start
: end
8212 * Get the length of the text input value.
8214 * This could differ from the length of #getValue if the
8215 * value gets filtered
8217 * @return {number} Input length
8219 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
8220 return this.$input
[ 0 ].value
.length
;
8224 * Focus the input and select the entire text.
8228 OO
.ui
.TextInputWidget
.prototype.select = function () {
8229 return this.selectRange( 0, this.getInputLength() );
8233 * Focus the input and move the cursor to the start.
8237 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
8238 return this.selectRange( 0 );
8242 * Focus the input and move the cursor to the end.
8246 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
8247 return this.selectRange( this.getInputLength() );
8251 * Insert new content into the input.
8253 * @param {string} content Content to be inserted
8256 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
8258 range
= this.getRange(),
8259 value
= this.getValue();
8261 start
= Math
.min( range
.from, range
.to
);
8262 end
= Math
.max( range
.from, range
.to
);
8264 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
8265 this.selectRange( start
+ content
.length
);
8270 * Insert new content either side of a selection.
8272 * @param {string} pre Content to be inserted before the selection
8273 * @param {string} post Content to be inserted after the selection
8276 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
8278 range
= this.getRange(),
8279 offset
= pre
.length
;
8281 start
= Math
.min( range
.from, range
.to
);
8282 end
= Math
.max( range
.from, range
.to
);
8284 this.selectRange( start
).insertContent( pre
);
8285 this.selectRange( offset
+ end
).insertContent( post
);
8287 this.selectRange( offset
+ start
, offset
+ end
);
8292 * Set the validation pattern.
8294 * The validation pattern is either a regular expression, a function, or the symbolic name of a
8295 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
8296 * value must contain only numbers).
8298 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
8299 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
8301 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
8302 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
8303 this.validate
= validate
;
8305 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
8310 * Sets the 'invalid' flag appropriately.
8312 * @param {boolean} [isValid] Optionally override validation result
8314 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
8316 setFlag = function ( valid
) {
8318 widget
.$input
.attr( 'aria-invalid', 'true' );
8320 widget
.$input
.removeAttr( 'aria-invalid' );
8322 widget
.setFlags( { invalid
: !valid
} );
8325 if ( isValid
!== undefined ) {
8328 this.getValidity().then( function () {
8337 * Check if a value is valid.
8339 * This method returns a promise that resolves with a boolean `true` if the current value is
8340 * considered valid according to the supplied {@link #validate validation pattern}.
8343 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
8345 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
8348 if ( this.validate
instanceof Function
) {
8349 result
= this.validate( this.getValue() );
8350 if ( result
&& $.isFunction( result
.promise
) ) {
8351 return result
.promise();
8353 return $.Deferred().resolve( !!result
).promise();
8356 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
8361 * Get the validity of current value.
8363 * This method returns a promise that resolves if the value is valid and rejects if
8364 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
8366 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
8368 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
8371 function rejectOrResolve( valid
) {
8373 return $.Deferred().resolve().promise();
8375 return $.Deferred().reject().promise();
8379 if ( this.validate
instanceof Function
) {
8380 result
= this.validate( this.getValue() );
8381 if ( result
&& $.isFunction( result
.promise
) ) {
8382 return result
.promise().then( function ( valid
) {
8383 return rejectOrResolve( valid
);
8386 return rejectOrResolve( result
);
8389 return rejectOrResolve( this.getValue().match( this.validate
) );
8394 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
8396 * @param {string} labelPosition Label position, 'before' or 'after'
8399 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
8400 this.labelPosition
= labelPosition
;
8401 this.updatePosition();
8406 * Update the position of the inline label.
8408 * This method is called by #setLabelPosition, and can also be called on its own if
8409 * something causes the label to be mispositioned.
8413 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
8414 var after
= this.labelPosition
=== 'after';
8417 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
8418 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
8420 this.valCache
= null;
8421 this.scrollWidth
= null;
8423 this.positionLabel();
8429 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
8430 * already empty or when it's not editable.
8432 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
8433 if ( this.type
=== 'search' ) {
8434 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
8435 this.setIndicator( null );
8437 this.setIndicator( 'clear' );
8443 * Position the label by setting the correct padding on the input.
8448 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
8449 var after
, rtl
, property
;
8452 // Clear old values if present
8454 'padding-right': '',
8459 this.$element
.append( this.$label
);
8461 this.$label
.detach();
8465 after
= this.labelPosition
=== 'after';
8466 rtl
= this.$element
.css( 'direction' ) === 'rtl';
8467 property
= after
=== rtl
? 'padding-left' : 'padding-right';
8469 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
8477 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8478 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8479 if ( state
.scrollTop
!== undefined ) {
8480 this.$input
.scrollTop( state
.scrollTop
);
8485 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
8486 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
8487 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
8489 * - by typing a value in the text input field. If the value exactly matches the value of a menu
8490 * option, that option will appear to be selected.
8491 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
8494 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8496 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
8499 * // Example: A ComboBoxInputWidget.
8500 * var comboBox = new OO.ui.ComboBoxInputWidget( {
8501 * label: 'ComboBoxInputWidget',
8502 * value: 'Option 1',
8505 * new OO.ui.MenuOptionWidget( {
8507 * label: 'Option One'
8509 * new OO.ui.MenuOptionWidget( {
8511 * label: 'Option Two'
8513 * new OO.ui.MenuOptionWidget( {
8515 * label: 'Option Three'
8517 * new OO.ui.MenuOptionWidget( {
8519 * label: 'Option Four'
8521 * new OO.ui.MenuOptionWidget( {
8523 * label: 'Option Five'
8528 * $( 'body' ).append( comboBox.$element );
8530 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
8533 * @extends OO.ui.TextInputWidget
8536 * @param {Object} [config] Configuration options
8537 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8538 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
8539 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8540 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8541 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8543 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
8544 // Configuration initialization
8545 config
= $.extend( {
8548 // For backwards-compatibility with ComboBoxWidget config
8549 $.extend( config
, config
.input
);
8551 // Parent constructor
8552 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
8555 this.$overlay
= config
.$overlay
|| this.$element
;
8556 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
8560 $container
: this.$element
,
8561 disabled
: this.isDisabled()
8565 // For backwards-compatibility with ComboBoxWidget
8569 this.$indicator
.on( {
8570 click
: this.onIndicatorClick
.bind( this ),
8571 keypress
: this.onIndicatorKeyPress
.bind( this )
8573 this.connect( this, {
8574 change
: 'onInputChange',
8575 enter
: 'onInputEnter'
8577 this.menu
.connect( this, {
8578 choose
: 'onMenuChoose',
8579 add
: 'onMenuItemsChange',
8580 remove
: 'onMenuItemsChange'
8586 'aria-autocomplete': 'list'
8588 // Do not override options set via config.menu.items
8589 if ( config
.options
!== undefined ) {
8590 this.setOptions( config
.options
);
8592 // Extra class for backwards-compatibility with ComboBoxWidget
8593 this.$element
.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
8594 this.$overlay
.append( this.menu
.$element
);
8595 this.onMenuItemsChange();
8600 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
8605 * Get the combobox's menu.
8606 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
8608 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
8613 * Get the combobox's text input widget.
8614 * @return {OO.ui.TextInputWidget} Text input widget
8616 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
8621 * Handle input change events.
8624 * @param {string} value New value
8626 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
8627 var match
= this.menu
.getItemFromData( value
);
8629 this.menu
.selectItem( match
);
8630 if ( this.menu
.getHighlightedItem() ) {
8631 this.menu
.highlightItem( match
);
8634 if ( !this.isDisabled() ) {
8635 this.menu
.toggle( true );
8640 * Handle mouse click events.
8643 * @param {jQuery.Event} e Mouse click event
8645 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorClick = function ( e
) {
8646 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8648 this.$input
[ 0 ].focus();
8654 * Handle key press events.
8657 * @param {jQuery.Event} e Key press event
8659 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorKeyPress = function ( e
) {
8660 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
8662 this.$input
[ 0 ].focus();
8668 * Handle input enter events.
8672 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
8673 if ( !this.isDisabled() ) {
8674 this.menu
.toggle( false );
8679 * Handle menu choose events.
8682 * @param {OO.ui.OptionWidget} item Chosen item
8684 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
8685 this.setValue( item
.getData() );
8689 * Handle menu item change events.
8693 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
8694 var match
= this.menu
.getItemFromData( this.getValue() );
8695 this.menu
.selectItem( match
);
8696 if ( this.menu
.getHighlightedItem() ) {
8697 this.menu
.highlightItem( match
);
8699 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
8705 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
8707 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8710 this.menu
.setDisabled( this.isDisabled() );
8717 * Set the options available for this input.
8719 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8722 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
8725 .addItems( options
.map( function ( opt
) {
8726 return new OO
.ui
.MenuOptionWidget( {
8728 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
8737 * @deprecated Use OO.ui.ComboBoxInputWidget instead.
8739 OO
.ui
.ComboBoxWidget
= OO
.ui
.ComboBoxInputWidget
;
8742 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8743 * which is a widget that is specified by reference before any optional configuration settings.
8745 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8747 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8748 * A left-alignment is used for forms with many fields.
8749 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8750 * A right-alignment is used for long but familiar forms which users tab through,
8751 * verifying the current field with a quick glance at the label.
8752 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8753 * that users fill out from top to bottom.
8754 * - **inline**: The label is placed after the field-widget and aligned to the left.
8755 * An inline-alignment is best used with checkboxes or radio buttons.
8757 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8758 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8760 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8762 * @extends OO.ui.Layout
8763 * @mixins OO.ui.mixin.LabelElement
8764 * @mixins OO.ui.mixin.TitledElement
8767 * @param {OO.ui.Widget} fieldWidget Field widget
8768 * @param {Object} [config] Configuration options
8769 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8770 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
8771 * The array may contain strings or OO.ui.HtmlSnippet instances.
8772 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
8773 * The array may contain strings or OO.ui.HtmlSnippet instances.
8774 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
8775 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
8776 * For important messages, you are advised to use `notices`, as they are always shown.
8778 * @throws {Error} An error is thrown if no widget is specified
8780 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
8781 var hasInputWidget
, div
;
8783 // Allow passing positional parameters inside the config object
8784 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
8785 config
= fieldWidget
;
8786 fieldWidget
= config
.fieldWidget
;
8789 // Make sure we have required constructor arguments
8790 if ( fieldWidget
=== undefined ) {
8791 throw new Error( 'Widget not found' );
8794 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
8796 // Configuration initialization
8797 config
= $.extend( { align
: 'left' }, config
);
8799 // Parent constructor
8800 OO
.ui
.FieldLayout
.parent
.call( this, config
);
8802 // Mixin constructors
8803 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8804 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8807 this.fieldWidget
= fieldWidget
;
8810 this.$field
= $( '<div>' );
8811 this.$messages
= $( '<ul>' );
8812 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
8814 if ( config
.help
) {
8815 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
8816 classes
: [ 'oo-ui-fieldLayout-help' ],
8822 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
8823 div
.html( config
.help
.toString() );
8825 div
.text( config
.help
);
8827 this.popupButtonWidget
.getPopup().$body
.append(
8828 div
.addClass( 'oo-ui-fieldLayout-help-content' )
8830 this.$help
= this.popupButtonWidget
.$element
;
8832 this.$help
= $( [] );
8836 if ( hasInputWidget
) {
8837 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
8839 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
8843 .addClass( 'oo-ui-fieldLayout' )
8844 .append( this.$help
, this.$body
);
8845 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
8846 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
8848 .addClass( 'oo-ui-fieldLayout-field' )
8849 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
8850 .append( this.fieldWidget
.$element
);
8852 this.setErrors( config
.errors
|| [] );
8853 this.setNotices( config
.notices
|| [] );
8854 this.setAlignment( config
.align
);
8859 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
8860 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
8861 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
8866 * Handle field disable events.
8869 * @param {boolean} value Field is disabled
8871 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
8872 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
8876 * Handle label mouse click events.
8879 * @param {jQuery.Event} e Mouse click event
8881 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
8882 this.fieldWidget
.simulateLabelClick();
8887 * Get the widget contained by the field.
8889 * @return {OO.ui.Widget} Field widget
8891 OO
.ui
.FieldLayout
.prototype.getField = function () {
8892 return this.fieldWidget
;
8897 * @param {string} kind 'error' or 'notice'
8898 * @param {string|OO.ui.HtmlSnippet} text
8901 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
8902 var $listItem
, $icon
, message
;
8903 $listItem
= $( '<li>' );
8904 if ( kind
=== 'error' ) {
8905 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
8906 } else if ( kind
=== 'notice' ) {
8907 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
8911 message
= new OO
.ui
.LabelWidget( { label
: text
} );
8913 .append( $icon
, message
.$element
)
8914 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
8919 * Set the field alignment mode.
8922 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8925 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
8926 if ( value
!== this.align
) {
8927 // Default to 'left'
8928 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
8932 if ( value
=== 'inline' ) {
8933 this.$body
.append( this.$field
, this.$label
);
8935 this.$body
.append( this.$label
, this.$field
);
8937 // Set classes. The following classes can be used here:
8938 // * oo-ui-fieldLayout-align-left
8939 // * oo-ui-fieldLayout-align-right
8940 // * oo-ui-fieldLayout-align-top
8941 // * oo-ui-fieldLayout-align-inline
8943 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
8945 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
8953 * Set the list of error messages.
8955 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
8956 * The array may contain strings or OO.ui.HtmlSnippet instances.
8959 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
8960 this.errors
= errors
.slice();
8961 this.updateMessages();
8966 * Set the list of notice messages.
8968 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
8969 * The array may contain strings or OO.ui.HtmlSnippet instances.
8972 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
8973 this.notices
= notices
.slice();
8974 this.updateMessages();
8979 * Update the rendering of error and notice messages.
8983 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
8985 this.$messages
.empty();
8987 if ( this.errors
.length
|| this.notices
.length
) {
8988 this.$body
.after( this.$messages
);
8990 this.$messages
.remove();
8994 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
8995 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
8997 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
8998 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
9003 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9004 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9005 * is required and is specified before any optional configuration settings.
9007 * Labels can be aligned in one of four ways:
9009 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9010 * A left-alignment is used for forms with many fields.
9011 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9012 * A right-alignment is used for long but familiar forms which users tab through,
9013 * verifying the current field with a quick glance at the label.
9014 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9015 * that users fill out from top to bottom.
9016 * - **inline**: The label is placed after the field-widget and aligned to the left.
9017 * An inline-alignment is best used with checkboxes or radio buttons.
9019 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9020 * text is specified.
9023 * // Example of an ActionFieldLayout
9024 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
9025 * new OO.ui.TextInputWidget( {
9026 * placeholder: 'Field widget'
9028 * new OO.ui.ButtonWidget( {
9032 * label: 'An ActionFieldLayout. This label is aligned top',
9034 * help: 'This is help text'
9038 * $( 'body' ).append( actionFieldLayout.$element );
9041 * @extends OO.ui.FieldLayout
9044 * @param {OO.ui.Widget} fieldWidget Field widget
9045 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9047 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
9048 // Allow passing positional parameters inside the config object
9049 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9050 config
= fieldWidget
;
9051 fieldWidget
= config
.fieldWidget
;
9052 buttonWidget
= config
.buttonWidget
;
9055 // Parent constructor
9056 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
9059 this.buttonWidget
= buttonWidget
;
9060 this.$button
= $( '<div>' );
9061 this.$input
= $( '<div>' );
9065 .addClass( 'oo-ui-actionFieldLayout' );
9067 .addClass( 'oo-ui-actionFieldLayout-button' )
9068 .append( this.buttonWidget
.$element
);
9070 .addClass( 'oo-ui-actionFieldLayout-input' )
9071 .append( this.fieldWidget
.$element
);
9073 .append( this.$input
, this.$button
);
9078 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
9081 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9082 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9083 * configured with a label as well. For more information and examples,
9084 * please see the [OOjs UI documentation on MediaWiki][1].
9087 * // Example of a fieldset layout
9088 * var input1 = new OO.ui.TextInputWidget( {
9089 * placeholder: 'A text input field'
9092 * var input2 = new OO.ui.TextInputWidget( {
9093 * placeholder: 'A text input field'
9096 * var fieldset = new OO.ui.FieldsetLayout( {
9097 * label: 'Example of a fieldset layout'
9100 * fieldset.addItems( [
9101 * new OO.ui.FieldLayout( input1, {
9102 * label: 'Field One'
9104 * new OO.ui.FieldLayout( input2, {
9105 * label: 'Field Two'
9108 * $( 'body' ).append( fieldset.$element );
9110 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9113 * @extends OO.ui.Layout
9114 * @mixins OO.ui.mixin.IconElement
9115 * @mixins OO.ui.mixin.LabelElement
9116 * @mixins OO.ui.mixin.GroupElement
9119 * @param {Object} [config] Configuration options
9120 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9122 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
9123 // Configuration initialization
9124 config
= config
|| {};
9126 // Parent constructor
9127 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
9129 // Mixin constructors
9130 OO
.ui
.mixin
.IconElement
.call( this, config
);
9131 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9132 OO
.ui
.mixin
.GroupElement
.call( this, config
);
9134 if ( config
.help
) {
9135 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9136 classes
: [ 'oo-ui-fieldsetLayout-help' ],
9141 this.popupButtonWidget
.getPopup().$body
.append(
9143 .text( config
.help
)
9144 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9146 this.$help
= this.popupButtonWidget
.$element
;
9148 this.$help
= $( [] );
9153 .addClass( 'oo-ui-fieldsetLayout' )
9154 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
9155 if ( Array
.isArray( config
.items
) ) {
9156 this.addItems( config
.items
);
9162 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
9163 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
9164 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
9165 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
9168 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9169 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9170 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9171 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9173 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9174 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9175 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9176 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9177 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9178 * often have simplified APIs to match the capabilities of HTML forms.
9179 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9181 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9182 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9185 * // Example of a form layout that wraps a fieldset layout
9186 * var input1 = new OO.ui.TextInputWidget( {
9187 * placeholder: 'Username'
9189 * var input2 = new OO.ui.TextInputWidget( {
9190 * placeholder: 'Password',
9193 * var submit = new OO.ui.ButtonInputWidget( {
9197 * var fieldset = new OO.ui.FieldsetLayout( {
9198 * label: 'A form layout'
9200 * fieldset.addItems( [
9201 * new OO.ui.FieldLayout( input1, {
9202 * label: 'Username',
9205 * new OO.ui.FieldLayout( input2, {
9206 * label: 'Password',
9209 * new OO.ui.FieldLayout( submit )
9211 * var form = new OO.ui.FormLayout( {
9212 * items: [ fieldset ],
9213 * action: '/api/formhandler',
9216 * $( 'body' ).append( form.$element );
9219 * @extends OO.ui.Layout
9220 * @mixins OO.ui.mixin.GroupElement
9223 * @param {Object} [config] Configuration options
9224 * @cfg {string} [method] HTML form `method` attribute
9225 * @cfg {string} [action] HTML form `action` attribute
9226 * @cfg {string} [enctype] HTML form `enctype` attribute
9227 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9229 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
9232 // Configuration initialization
9233 config
= config
|| {};
9235 // Parent constructor
9236 OO
.ui
.FormLayout
.parent
.call( this, config
);
9238 // Mixin constructors
9239 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9242 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
9244 // Make sure the action is safe
9245 action
= config
.action
;
9246 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
9247 action
= './' + action
;
9252 .addClass( 'oo-ui-formLayout' )
9254 method
: config
.method
,
9256 enctype
: config
.enctype
9258 if ( Array
.isArray( config
.items
) ) {
9259 this.addItems( config
.items
);
9265 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
9266 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
9271 * A 'submit' event is emitted when the form is submitted.
9276 /* Static Properties */
9278 OO
.ui
.FormLayout
.static.tagName
= 'form';
9283 * Handle form submit events.
9286 * @param {jQuery.Event} e Submit event
9289 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
9290 if ( this.emit( 'submit' ) ) {
9296 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9297 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9300 * // Example of a panel layout
9301 * var panel = new OO.ui.PanelLayout( {
9305 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9307 * $( 'body' ).append( panel.$element );
9310 * @extends OO.ui.Layout
9313 * @param {Object} [config] Configuration options
9314 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9315 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
9316 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
9317 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
9319 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
9320 // Configuration initialization
9321 config
= $.extend( {
9328 // Parent constructor
9329 OO
.ui
.PanelLayout
.parent
.call( this, config
);
9332 this.$element
.addClass( 'oo-ui-panelLayout' );
9333 if ( config
.scrollable
) {
9334 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
9336 if ( config
.padded
) {
9337 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
9339 if ( config
.expanded
) {
9340 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
9342 if ( config
.framed
) {
9343 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
9349 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
9354 * Focus the panel layout
9356 * The default implementation just focuses the first focusable element in the panel
9358 OO
.ui
.PanelLayout
.prototype.focus = function () {
9359 OO
.ui
.findFocusable( this.$element
).focus();
9363 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
9364 * items), with small margins between them. Convenient when you need to put a number of block-level
9365 * widgets on a single line next to each other.
9367 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
9370 * // HorizontalLayout with a text input and a label
9371 * var layout = new OO.ui.HorizontalLayout( {
9373 * new OO.ui.LabelWidget( { label: 'Label' } ),
9374 * new OO.ui.TextInputWidget( { value: 'Text' } )
9377 * $( 'body' ).append( layout.$element );
9380 * @extends OO.ui.Layout
9381 * @mixins OO.ui.mixin.GroupElement
9384 * @param {Object} [config] Configuration options
9385 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
9387 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
9388 // Configuration initialization
9389 config
= config
|| {};
9391 // Parent constructor
9392 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
9394 // Mixin constructors
9395 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9398 this.$element
.addClass( 'oo-ui-horizontalLayout' );
9399 if ( Array
.isArray( config
.items
) ) {
9400 this.addItems( config
.items
);
9406 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
9407 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);